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

351 lines
10 KiB
C

/* 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;
}