Files
MeowOS-SDK/tuku/logic/reader/page_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

348 lines
11 KiB
C

/* logic/reader/page_source.c — 页源契约实现(纯 C):目录册 + 图片头探测。
*
* 契约与两种消费方式的说明见 page_source.h。本文件不含 SDK 依赖:解码/释放由
* UI 层注回回调,因此可在宿主测试里用假回调覆盖「空册/越界/重复 open-close/首末页」。
*/
#include "logic/reader/page_source.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
/* ============================================================ */
/* 契约包装 */
/* ============================================================ */
int mr_book_valid(const mr_book *b)
{
return b && b->ops && b->ops->count && b->ops->open;
}
int mr_book_count(const mr_book *b)
{
if (!mr_book_valid(b)) return 0;
int n = b->ops->count(b->ctx);
return n > 0 ? n : 0;
}
int mr_book_meta(const mr_book *b, int index, int *w, int *h)
{
if (w) *w = 0;
if (h) *h = 0;
if (!mr_book_valid(b) || !b->ops->meta) return 0;
if (index < 0 || index >= mr_book_count(b)) return 0;
return b->ops->meta(b->ctx, index, w, h) ? 1 : 0;
}
meow_img *mr_book_open(mr_book *b, int index)
{
if (!mr_book_valid(b)) return NULL;
if (index < 0 || index >= mr_book_count(b)) return NULL; /* 越界:契约边界 */
return b->ops->open(b->ctx, index);
}
void mr_book_close(mr_book *b, meow_img *img)
{
if (!img) return;
if (!mr_book_valid(b) || !b->ops->close) return;
b->ops->close(b->ctx, img);
}
void mr_book_destroy(mr_book *b)
{
if (!b) return;
if (b->ops && b->ops->destroy && b->ctx) b->ops->destroy(b->ctx);
b->ops = NULL;
b->ctx = NULL;
}
/* ============================================================ */
/* 目录册 */
/* ============================================================ */
static int name_cmp_qsort(const void *pa, const void *pb)
{
const char *a = *(const char *const *)pa;
const char *b = *(const char *const *)pb;
return mr_natural_cmp(a, b);
}
void mr_dir_book_path(const mr_dir_book *db, int index, char *out, int outsz)
{
if (!out || outsz <= 0) return;
out[0] = '\0';
if (!db || index < 0 || index >= db->count || !db->names[index]) return;
size_t dl = strlen(db->dir);
if (dl > 0 && db->dir[dl - 1] == '/')
snprintf(out, (size_t)outsz, "%s%s", db->dir, db->names[index]);
else
snprintf(out, (size_t)outsz, "%s/%s", db->dir, db->names[index]);
}
void mr_dir_book_free(mr_dir_book *db)
{
if (!db) return;
for (int i = 0; i < db->count; i++) free(db->names[i]);
free(db->names);
db->names = NULL;
db->count = 0;
}
int mr_dir_book_init(mr_dir_book *db, const char *dir, int maxdim,
mr_book_load_fn load, mr_book_unload_fn unload, void *ud)
{
if (!db) return -1;
memset(db, 0, sizeof *db);
if (!dir || !dir[0] || !load || !unload) return -1;
if (maxdim <= 0) maxdim = 720;
if (strlen(dir) >= sizeof db->dir) return -1;
DIR *d = opendir(dir);
if (!d) return -1;
snprintf(db->dir, sizeof db->dir, "%s", dir);
db->maxdim = maxdim;
db->load = load;
db->unload = unload;
db->ud = ud;
int cap = 0;
struct dirent *de;
while ((de = readdir(d)) != NULL) {
const char *name = de->d_name;
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
if (name[0] == '.') continue; /* 隐藏文件不参与成册 */
if (!mr_path_is_image(name)) continue;
char full[MR_PATH_MAX];
size_t dl = strlen(db->dir);
int n = (dl > 0 && db->dir[dl - 1] == '/')
? snprintf(full, sizeof full, "%s%s", db->dir, name)
: snprintf(full, sizeof full, "%s/%s", db->dir, name);
if (n <= 0 || n >= (int)sizeof full) continue;
struct stat st;
if (lstat(full, &st) != 0 || !S_ISREG(st.st_mode)) continue; /* 跳过符号链接/目录 */
if (db->count >= MR_BOOK_MAX_PAGES) break;
if (db->count >= cap) {
int nc = cap ? cap * 2 : 32;
char **nn = (char **)realloc(db->names, (size_t)nc * sizeof *nn);
if (!nn) { mr_dir_book_free(db); closedir(d); return -1; }
db->names = nn;
cap = nc;
}
size_t len = strlen(name);
db->names[db->count] = (char *)malloc(len + 1);
if (!db->names[db->count]) { mr_dir_book_free(db); closedir(d); return -1; }
memcpy(db->names[db->count], name, len + 1);
db->count++;
}
closedir(d);
if (db->count > 1)
qsort(db->names, (size_t)db->count, sizeof *db->names, name_cmp_qsort);
return db->count;
}
/* ============================================================ */
/* 图片头探测(不解码像素) */
/* ------------------------------------------------------------
* 同一套解析同时服务「文件」(目录册,流式读不占内存)和「内存缓冲」
* (档案册: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(probe_src *s, int *w, int *h)
{
unsigned char hdr[24];
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);
if (ww == 0 || hh == 0) return 0;
*w = (int)ww;
*h = (int)hh;
return 1;
}
static int probe_jpeg(probe_src *s, int *w, int *h)
{
unsigned char b[2];
if (!ps_read(s, b, 2) || b[0] != 0xFF || b[1] != 0xD8) return 0;
for (;;) {
int c = ps_getc(s);
if (c == EOF) return 0;
if (c != 0xFF) continue;
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 (!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 || !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;
*w = ww;
*h = hh;
return 1;
}
if (!ps_skip(s, len - 2)) return 0;
}
}
static int probe_bmp(probe_src *s, int *w, int *h)
{
unsigned char hdr[26];
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));
int hh = (int)((unsigned)hdr[22] | ((unsigned)hdr[23] << 8) |
((unsigned)hdr[24] << 16) | ((unsigned)hdr[25] << 24));
if (hh < 0) hh = -hh; /* 自底向上 BMP */
if (ww <= 0 || hh <= 0) return 0;
*w = ww;
*h = hh;
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;
if (h) *h = 0;
if (!path || !path[0] || !w || !h) return 0;
FILE *f = fopen(path, "rb");
if (!f) return 0;
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;
}
/* ============================================================ */
/* mr_book_ops 适配器(目录册) */
/* ============================================================ */
static int dir_ops_count(void *ctx)
{
mr_dir_book *db = (mr_dir_book *)ctx;
return db ? db->count : 0;
}
static int dir_ops_meta(void *ctx, int index, int *w, int *h)
{
mr_dir_book *db = (mr_dir_book *)ctx;
if (w) *w = 0;
if (h) *h = 0;
if (!db || index < 0 || index >= db->count) return 0;
char path[MR_PATH_MAX];
mr_dir_book_path(db, index, path, (int)sizeof path);
return mr_probe_size(path, w, h);
}
static meow_img *dir_ops_open(void *ctx, int index)
{
mr_dir_book *db = (mr_dir_book *)ctx;
if (!db || index < 0 || index >= db->count) return NULL;
char path[MR_PATH_MAX];
mr_dir_book_path(db, index, path, (int)sizeof path);
if (!path[0]) return NULL;
return db->load(path, db->ud, db->maxdim);
}
static void dir_ops_close(void *ctx, meow_img *img)
{
mr_dir_book *db = (mr_dir_book *)ctx;
if (db && db->unload && img) db->unload(img);
}
static void dir_ops_destroy(void *ctx)
{
mr_dir_book *db = (mr_dir_book *)ctx;
if (db) mr_dir_book_free(db);
}
static const mr_book_ops g_dir_ops = {
dir_ops_count, dir_ops_meta, dir_ops_open, dir_ops_close, dir_ops_destroy,
};
void mr_dir_book_bind(mr_book *b, mr_dir_book *db)
{
if (!b) return;
b->ops = db ? &g_dir_ops : NULL;
b->ctx = db;
}