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

463 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* logic/library.c — 书库文件管理器模型:单层枚举 + 纯内存行合成(纯 C)。
*
* 扫描(mr_library_scan)与 os.meow.files 的 fsx_scan_dir 同构:
* - 一次 opendir/readdir,逐项 lstat(只认目录、受支持图片、.cbz/.zip;符号链接跳过防环);
* - 隐藏项("." 开头,含 . ..)跳过;空目录/无图目录/系统目录照常列出;
* - 排序:目录优先,组内按 mr_natural_cmp 自然升序;
* - 副标题(类型 · 大小)在扫描期生成一次,渲染期不再做任何格式化/IO;
* - 条目到 MR_LIB_SCAN_MAX 后仍有条目 → 截断标注。
*
* 阅读器辅助(与列表模型解耦):mr_library_dir_has_images 单层判据 +
* mr_book_adjacent(_ex) 兄弟册衔接(M7/M11 阅读器翻到相邻册用)。
*/
#include "library.h"
#include <ctype.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#ifdef MR_LIBRARY_TEST_HOOKS
static mr_library_io_stats g_io;
void mr_library_test_io_reset(void)
{
memset(&g_io, 0, sizeof g_io);
}
void mr_library_test_io_get(mr_library_io_stats *out)
{
if (out) *out = g_io;
}
#define IO_OPEN() (g_io.opendirs++)
#define IO_READDIR() (g_io.readdirs++)
#define IO_LSTAT() (g_io.lstats++)
#else
#define IO_OPEN() ((void)0)
#define IO_READDIR() ((void)0)
#define IO_LSTAT() ((void)0)
#endif
/* 系统目录不参与**册间衔接**(M7 兄弟册表);浏览本身已放开(不隐藏)。 */
static int ignored_dir_name(const char *name)
{
static const char *const skip[] = { "MeowAPPs", "MeowData", "MeowOS", "lost+found" };
if (!name || name[0] == '.') return 1;
for (unsigned i = 0; i < sizeof skip / sizeof skip[0]; i++)
if (strcmp(name, skip[i]) == 0) return 1;
return 0;
}
static int copy_str(char *dst, int dstsz, const char *src)
{
if (!dst || dstsz <= 0) return -1;
if (!src) { dst[0] = '\0'; return 0; }
int n = (int)strlen(src);
if (n >= dstsz) return -1;
memcpy(dst, src, (size_t)n + 1);
return 0;
}
static int join_path(char *out, int outsz, const char *dir, const char *name)
{
if (!out || outsz <= 0 || !dir || !name) return -1;
int n;
size_t dl = strlen(dir);
if (dl > 0 && dir[dl - 1] == '/')
n = snprintf(out, (size_t)outsz, "%s%s", dir, name);
else
n = snprintf(out, (size_t)outsz, "%s/%s", dir, name);
return (n > 0 && n < outsz) ? 0 : -1;
}
int mr_path_is_image(const char *name)
{
if (!name) return 0;
const char *dot = strrchr(name, '.');
if (!dot || dot == name) return 0;
dot++;
return strcasecmp(dot, "png") == 0 || strcasecmp(dot, "jpg") == 0 ||
strcasecmp(dot, "jpeg") == 0 || strcasecmp(dot, "bmp") == 0;
}
int mr_path_is_archive(const char *name)
{
if (!name) return 0;
const char *dot = strrchr(name, '.');
if (!dot || dot == name) return 0;
dot++;
return strcasecmp(dot, "cbz") == 0 || strcasecmp(dot, "zip") == 0;
}
int mr_natural_cmp(const char *a, const char *b)
{
const unsigned char *pa = (const unsigned char *)a;
const unsigned char *pb = (const unsigned char *)b;
if (!a || !b) return (a == b) ? 0 : (a ? 1 : -1);
while (*pa && *pb) {
if (isdigit(*pa) && isdigit(*pb)) {
const unsigned char *za = pa, *zb = pb;
while (*pa == '0') pa++;
while (*pb == '0') pb++;
const unsigned char *da = pa, *db = pb;
while (isdigit(*pa)) pa++;
while (isdigit(*pb)) pb++;
size_t la = (size_t)(pa - da), lb = (size_t)(pb - db);
if (la != lb) return la < lb ? -1 : 1;
if (la > 0) {
int c = memcmp(da, db, la);
if (c != 0) return c < 0 ? -1 : 1;
}
size_t oa = (size_t)(da - za), ob = (size_t)(db - zb);
if (oa != ob) return oa < ob ? -1 : 1;
continue;
}
int ca = tolower(*pa), cb = tolower(*pb);
if (ca != cb) return ca < cb ? -1 : 1;
pa++;
pb++;
}
if (*pa) return 1;
if (*pb) return -1;
return strcmp(a, b) < 0 ? -1 : (strcmp(a, b) > 0 ? 1 : 0);
}
/* ============================================================ */
/* 扫描 / 排序 / 行合成 */
/* ============================================================ */
static void fmt_size(long long size, char *out, int outsz)
{
if (size < 0) size = 0;
static const char *const unit[] = { "B", "KB", "MB", "GB" };
double v = (double)size;
int i = 0;
while (v >= 1024.0 && i < 3) { v /= 1024.0; i++; }
if (i == 0) snprintf(out, (size_t)outsz, "%lld B", size);
else snprintf(out, (size_t)outsz, "%.1f %s", v, unit[i]);
}
/* 副标题只在扫描期生成一次(渲染期零格式化)。 */
static void format_subtitle(mr_library_entry *e)
{
char sz[32];
switch (e->kind) {
case MR_LIB_KIND_DIR:
snprintf(e->subtitle, sizeof e->subtitle, "文件夹");
break;
case MR_LIB_KIND_ARCHIVE:
fmt_size(e->size, sz, (int)sizeof sz);
snprintf(e->subtitle, sizeof e->subtitle, "档案册 · %s", sz);
break;
default:
fmt_size(e->size, sz, (int)sizeof sz);
snprintf(e->subtitle, sizeof e->subtitle, "图片 · %s", sz);
break;
}
}
void mr_library_free(mr_library *lib)
{
if (!lib) return;
free(lib->items);
lib->items = NULL;
lib->count = 0;
lib->cap = 0;
lib->truncated = 0;
}
static int lib_add(mr_library *lib, const mr_library_entry *e)
{
if (lib->count >= lib->cap) {
int cap = lib->cap ? lib->cap * 2 : 16;
mr_library_entry *ni = (mr_library_entry *)realloc(lib->items,
(size_t)cap * sizeof *ni);
if (!ni) return -1;
lib->items = ni;
lib->cap = cap;
}
lib->items[lib->count++] = *e;
return 0;
}
/* 目录优先;组内自然序(大小写不敏感 + 原始字节兜底,结果确定)。 */
static int entry_cmp(const void *pa, const void *pb)
{
const mr_library_entry *a = (const mr_library_entry *)pa;
const mr_library_entry *b = (const mr_library_entry *)pb;
int ad = (a->kind == MR_LIB_KIND_DIR), bd = (b->kind == MR_LIB_KIND_DIR);
if (ad != bd) return ad ? -1 : 1;
return mr_natural_cmp(a->name, b->name);
}
static int add_entry(mr_library *lib, const char *name, const char *full,
int kind, const struct stat *st)
{
mr_library_entry e;
memset(&e, 0, sizeof e);
if (copy_str(e.name, (int)sizeof e.name, name) != 0) return 0;
if (copy_str(e.path, (int)sizeof e.path, full) != 0) return 0;
e.kind = kind;
e.size = (kind == MR_LIB_KIND_DIR) ? 0 : (long long)st->st_size;
format_subtitle(&e);
return lib_add(lib, &e);
}
int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
{
(void)max_depth; /* 兼容旧签名:扫描不递归 */
if (!lib) return -1;
mr_library_free(lib);
if (!dir || !dir[0]) return -1;
IO_OPEN();
DIR *d = opendir(dir);
if (!d) return -1;
struct dirent *de;
while ((de = readdir(d)) != NULL) {
IO_READDIR();
const char *name = de->d_name;
if (name[0] == '.') continue; /* . .. 与隐藏项 */
char full[MR_PATH_MAX];
if (join_path(full, (int)sizeof full, dir, name) != 0) continue;
IO_LSTAT();
struct stat st;
if (lstat(full, &st) != 0) continue;
if (S_ISLNK(st.st_mode)) continue;
int kind;
if (S_ISDIR(st.st_mode)) kind = MR_LIB_KIND_DIR;
else if (!S_ISREG(st.st_mode)) continue;
else if (mr_path_is_image(name)) kind = MR_LIB_KIND_IMAGE;
else if (mr_path_is_archive(name)) kind = MR_LIB_KIND_ARCHIVE;
else continue;
if (lib->count >= MR_LIB_SCAN_MAX) { lib->truncated = 1; break; }
if (add_entry(lib, name, full, kind, &st) != 0) {
closedir(d);
mr_library_free(lib);
return -1;
}
}
closedir(d);
if (lib->count > 1)
qsort(lib->items, (size_t)lib->count, sizeof *lib->items, entry_cmp);
return lib->count;
}
int mr_library_row_at(const mr_library *lib, int idx, mr_library_row *out)
{
static char trunc_sub[96];
static int trunc_ready;
if (!lib || !out) return 0;
if (idx == lib->count && lib->truncated) {
if (!trunc_ready) {
/* 截断只是列表显示上限:图片仍能从任一可见图片进入阅读器后翻到,
* 文案不要让人误以为「没有更多图片」。 */
snprintf(trunc_sub, sizeof trunc_sub,
"仅显示前 %d 项(图片可在阅读器中翻到)", MR_LIB_SCAN_MAX);
trunc_ready = 1;
}
memset(out, 0, sizeof *out);
out->title = "已截断";
out->subtitle = trunc_sub;
out->disabled = 1;
out->index = -1;
return 1;
}
if (idx < 0 || idx >= lib->count) return 0;
const mr_library_entry *e = &lib->items[idx];
memset(out, 0, sizeof *out);
out->title = e->name;
out->subtitle = e->subtitle;
out->kind = e->kind;
out->index = idx;
return 1;
}
int mr_path_within_root(const char *path, const char *root)
{
if (!path || !path[0] || !root || !root[0]) return 0;
size_t rl = strlen(root);
while (rl > 1 && root[rl - 1] == '/') rl--;
if (rl == 1 && root[0] == '/') return path[0] == '/';
if (strncmp(path, root, rl) != 0) return 0;
char c = path[rl];
return c == '\0' || c == '/';
}
int mr_path_parent(const char *path, char *out, int outsz)
{
if (!path || !out || outsz <= 0) return -1;
size_t n = strlen(path);
while (n > 1 && path[n - 1] == '/') n--;
if (n <= 1) {
if (outsz < 2) return -1;
out[0] = '/';
out[1] = '\0';
return 0;
}
while (n > 0 && path[n - 1] != '/') n--;
while (n > 1 && path[n - 1] == '/') n--;
if (n >= (size_t)outsz) return -1;
memcpy(out, path, n);
out[n] = '\0';
return 0;
}
/* ============================================================ */
/* 册间衔接(M7 / M11) */
/* ============================================================ */
int mr_library_dir_has_images(const char *dir)
{
if (!dir || !dir[0]) return 0;
IO_OPEN();
DIR *d = opendir(dir);
if (!d) return 0;
int found = 0;
struct dirent *de;
while ((de = readdir(d)) != NULL) {
IO_READDIR();
const char *name = de->d_name;
if (name[0] == '.' || !mr_path_is_image(name)) continue;
char full[MR_PATH_MAX];
if (join_path(full, (int)sizeof full, dir, name) != 0) continue;
IO_LSTAT();
struct stat st;
if (lstat(full, &st) != 0 || !S_ISREG(st.st_mode)) continue;
found = 1;
break;
}
closedir(d);
return found;
}
static int name_ptr_cmp(const void *pa, const void *pb)
{
const char *a = *(const char *const *)pa;
const char *b = *(const char *const *)pb;
return mr_natural_cmp(a, b);
}
/* 兄弟册判据(M11):目录=直接含图;普通文件=档案册且有图片页。
* 两者同列,交给 name_ptr_cmp 自然序混排。 */
static int sibling_is_book(const char *full, const struct stat *st,
mr_lib_archive_fn probe, void *ud)
{
if (S_ISDIR(st->st_mode)) return mr_library_dir_has_images(full);
if (S_ISREG(st->st_mode)) {
const char *slash = strrchr(full, '/');
const char *name = slash ? slash + 1 : full;
if (!mr_path_is_archive(name)) return 0;
if (!probe) return 0;
int n = probe(full, ud);
return n > 0;
}
return 0;
}
int mr_book_adjacent_ex(const char *dir, int delta, char *out, int outsz,
mr_lib_archive_fn archive_pages, void *ud)
{
if (!out || outsz <= 0) return -1;
out[0] = '\0';
if (!dir || !dir[0] || (delta != 1 && delta != -1)) return -1;
char norm[MR_PATH_MAX];
size_t dl = strlen(dir);
while (dl > 1 && dir[dl - 1] == '/') dl--;
if (dl >= sizeof norm) return -1;
memcpy(norm, dir, dl);
norm[dl] = '\0';
const char *slash = strrchr(norm, '/');
if (!slash || !slash[1]) return -1;
const char *cur = slash + 1;
char parent[MR_PATH_MAX];
if (mr_path_parent(norm, parent, (int)sizeof parent) != 0) return -1;
DIR *d = opendir(parent);
if (!d) return 0;
char **names = NULL;
int n = 0, cap = 0;
struct dirent *de;
while ((de = readdir(d)) != NULL) {
const char *name = de->d_name;
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
if (ignored_dir_name(name)) continue;
char full[MR_PATH_MAX];
if (join_path(full, (int)sizeof full, parent, name) != 0) continue;
struct stat st;
if (lstat(full, &st) != 0) continue;
if (S_ISLNK(st.st_mode)) continue;
if (!sibling_is_book(full, &st, archive_pages, ud)) continue;
if (n >= cap) {
int nc = cap ? cap * 2 : 16;
char **nn = (char **)realloc(names, (size_t)nc * sizeof *nn);
if (!nn) break;
names = nn;
cap = nc;
}
size_t len = strlen(name);
names[n] = (char *)malloc(len + 1);
if (!names[n]) break;
memcpy(names[n], name, len + 1);
n++;
}
closedir(d);
int rc = 0;
if (n > 1) qsort(names, (size_t)n, sizeof *names, name_ptr_cmp);
int idx = -1;
for (int i = 0; i < n; i++)
if (strcmp(names[i], cur) == 0) { idx = i; break; }
int target = idx + delta;
if (idx >= 0 && target >= 0 && target < n) {
if (join_path(out, outsz, parent, names[target]) == 0) rc = 1;
}
for (int i = 0; i < n; i++) free(names[i]);
free(names);
return rc;
}
int mr_book_adjacent_rooted(const char *dir, int delta, const char *root,
mr_lib_archive_fn archive_pages, void *ud,
char *out, int outsz)
{
if (!out || outsz <= 0) return -1;
out[0] = '\0';
if (!dir || !dir[0] || !root || !root[0]) return -1;
/* 只允许在浏览根内翻册:当前册或相邻册越根一律当「没有相邻册」。 */
if (!mr_path_within_root(dir, root)) return 0;
int r = mr_book_adjacent_ex(dir, delta, out, outsz, archive_pages, ud);
if (r != 1) return r;
if (!mr_path_within_root(out, root)) {
out[0] = '\0';
return 0;
}
return 1;
}
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz)
{
return mr_book_adjacent_ex(dir, delta, out, outsz, NULL, NULL);
}