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