- 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 上限/ 启动清孤儿/续读角标/封面首图。
396 lines
13 KiB
C
396 lines
13 KiB
C
/* 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);
|
||
}
|