Files
MeowOS-SDK/tuku/logic/reader/archive_source.c
T
AskaEth 562cc44931 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 上限/
  启动清孤儿/续读角标/封面首图。
2026-11-05 11:58:12 +08:00

396 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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);
}