P1-1(会静默开错图):取消「书库从列表推导图片下标」。 - 新增 mr_reader_open_dir_at_name(dir, name):阅读器打开目录册后,在**自己刚枚举出的 全量目录册**(mr_dir_book,与翻页同一份枚举)里按 basename 定位起始页(先精确, 再大小写不敏感兜底;找不到退回续读/首页)。书库点击只传文件名。 - 删除 mr_library_image_index(不再有跨枚举的隐式契约)。 - 截断提示文案改为「仅显示前 2000 项(图片可在阅读器中翻到)」。 - 修复前(cb02722 探针,2100 张 4 位名夹具):rows=2000/truncated=1/ library_images=2000/reader_pages=2100/**mismatched=590**(首行 p1412 → 打开 p1411); 评审夹具为 1995/2000。修复后:2000 个可见图片行按名定位 **0 mismatch**, 2100 页全量可翻;26 次真实 UI 点开(首尾各 3 + 20 个确定性伪随机)逐张命中。 连带处理(评审 r1): 1. 删掉 reader_page_enter 的 c->view 死写(reopen 判定用 activity active/depth); 顺手删掉 library_page_create 里同款死写。 2. 阅读器册间衔接根锁定:新增 mr_path_within_root + mr_book_adjacent_rooted, book_switch 用浏览根(默认 MR_ROOT_DIR,测试可注入)钳制相邻册; 根外的兄弟册按「没有相邻册」处理。test_cont_adjacent A7 验证「明文 _ex 会越根、 rooted 拒绝、根内翻册不受影响」;test_mode_switch_host H6 验证条漫末页不越根。 3. test_reader_perf 不再依赖顶栏 tap 命中时序:模式切换改走测试钩子 mr_reader_test_toggle_mode()(与顶栏动作同一个 mode_set),负载下稳定。 4. page_cache 注释去掉「缩略图缓存/缩略图线程」过时描述(只改文字)。 回归:run_tests.sh(library 81 / nav 74 / lazy 57 / perf 25 / … / open_book 含 2100 用例) 与 run_webtoon_tests.sh(含 A7/H6)全 PASS;交叉构建零警告。
367 lines
11 KiB
C
367 lines
11 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;
|
|
}
|
|
|
|
int mr_cache_is_page_name(const char *name)
|
|
{
|
|
return name && strncmp(name, MR_CACHE_PREFIX, strlen(MR_CACHE_PREFIX)) == 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);
|
|
}
|
|
|
|
/* 腾出 len 字节:页数或字节先到上限就淘汰最旧。单条 len 本身超过
|
|
* MR_CACHE_MAX_BYTES 时会清空其它条目、让这一条独占(文档化的一次性溢出,
|
|
* 上限由页源单页 64MiB 约束)——大页仍可读,不因预算拒绝落缓存。 */
|
|
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);
|
|
/* 命中也要 stat 校验(P2-1 纵深防御):外部清理、掉电残留、或别的 LRU 误删
|
|
* 都会让索引指向不存在的文件;假命中会让页源返回死路径且永不自愈。缺失或
|
|
* 尺寸不符就从索引移除、按未命中处理,让调用方重新解压。 */
|
|
struct stat st;
|
|
int exists = stat(path, &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0;
|
|
if (i >= 0) {
|
|
if (!exists || (long long)st.st_size != g_idx[i].size) {
|
|
idx_remove_locked(i);
|
|
pthread_mutex_unlock(&g_lock);
|
|
return 0;
|
|
}
|
|
g_tick++;
|
|
g_idx[i].used = g_tick;
|
|
pthread_mutex_unlock(&g_lock);
|
|
return 1;
|
|
}
|
|
/* 索引里没有(外部写入/索引满):磁盘上有就算命中,能装进索引就装。 */
|
|
if (exists && 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 exists ? 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;
|
|
}
|