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);
}
+350
View File
@@ -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;
}
+68
View File
@@ -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 */
+81 -16
View File
@@ -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;
+78
View File
@@ -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
}