/* logic/reader/page_cache.c — 解压页磁盘缓存实现,见 page_cache.h 的纪律说明。 */ #include "logic/reader/page_cache.h" #include #include #include #include #include #include #include #include #include #include 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; }