Files
MeowOS-SDK/tuku/logic/reader/page_cache.c
T
AskaEth 4befbfe14c 图库: 书库点图改按文件名定位(修 P1 截断错位)+ 册间衔接根锁定(M18 评审 r1)
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;交叉构建零警告。
2026-11-05 20:26:40 +08:00

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