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:
AskaEth committed 2026-11-05 11:58:12 +08:00
1 parent 19620a7957
commit 562cc44931
16 files changed
+2330 -108

No files matched your search

+395
View File
@@ -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);
}