M29 只解决了「回书架不重扫」;首次进书架仍要全量扫 434 目录(真机 3.0–3.4s, 其中 opendir+readdir 占 96%)。本单把扫描结果 + 每个扫描目录的 mtime 存到 MeowData/os.meow.reader/index/shelf.idx,冷启动逐目录 stat 比对 mtime,命中即 完全跳过 readdir;任何异常一律 fail-open 全量重扫。 先量再选(任务 1):宿主 434 目录/595 本/51 rar 虚拟库(贴真机量级) - 434 次目录 stat = 0.48ms/pass(1.1µs/次,不含 readdir); - 读 72KB 索引 = 0.009ms;全量扫描 3.54ms;索引命中 1.29ms(ts_fn=NULL)。 真机按 M28 实测 1079 次 stat=160–170ms(≈150µs/次)外推:434 次 ≈ 65ms, 远低于「可能不可行」的 1s 阈值 → 选「校验全部扫描目录 mtime」(非只 level-1)。 strace 断言:indexonly 载入 0 次 getdents64、恰好 434 次 stat(真的没 readdir)。 生产路径 ≈ 65ms(stat) + <20ms(索引读) + 40–50ms(rdprog×595) + <20ms(build_rows) ≈ 0.13–0.15s(对比 3.0s)。 索引格式 v1(手工小端序列化,不依赖结构体布局): header(magic/version/header_size/root_len) + root + dir 记录×(rel,mtime_s, mtime_ns,books_here) + book 记录×(rel,size,fmt) + trailer(ndirs,nbooks,flags, depth_skipped,crc32,magic)。truncated/depth_skipped 存在 flags/字段里,载入时 如实重建提示条;**不存阅读进度 ts**,载入后每本由 rdprog_load 现读再排序。 失效语义(存每个扫描过的目录,不只 level-1): - level-2 新增 / level-3 深处新增 / 删书 → 该目录 mtime 变 → 重扫; - 一级目录改名 → 父目录 mtime 变 + 旧目录 stat ENOENT → 重扫; - 超深(>6 层)未扫描子树不在索引内,其内部变化不自动失效(本来就不显示), 点「刷新」更新 depth_skipped 计数;mtime 精度受限(exFAT 10ms/FAT32 2s)的 同刻度变更也可能漏判,点「刷新」必可纠正。 M29 语义不变:首次进书架=索引或全扫;回书架=不扫;刷新=删索引+全扫(之后原子 写回新索引)。 安全/健壮(宁可慢不可错): - 写:shelf.idx.tmp 全量写完(尽力 fsync)后 rename;中途被杀只剩 tmp,正式索引 要么旧要么完整新;rename 覆盖失败先删旧再改名;fflush/写错即放弃,绝不影响书架。 - 读:O_NOFOLLOW|O_NONBLOCK + S_ISREG(FIFO/符号链接直接拒绝不阻塞);CRC32 覆盖 全部记录;rel 做绝对路径/“..”/内嵌 NUL/长度校验;版本/根路径/计数/逐目录 mtime 任一不符即重扫;载入崩溃路径全部 goto 清理,不改书架可用性。 - writer_open 失败(卡满/写保护/无 MeowData)→ 扫描不采集目录、照常出书; commit 失败(含只读目录 rename 失败)不留正式索引。 测试:test/shelf_index_smoke 154 pass(含四种变更场景失效、截断/字节翻转/垃圾/ 缺失/FIFO/符号链接/版本不符/根路径不符、ts 现读、truncated/depth_skipped 重建、 只读目录写失败、空 Books、root 缺失、.tmp 残渣),ASan+UBSan 干净; test/shelf_index_bench 模拟 434 目录/595 本/51 rar:scan/load 均 595 本、 truncated=0/depth_skipped=0、索引 72,156B。build_reader.sh 全绿 0 warning (core 6846 / seg 19 / chapter 71 / shelf_group 63 / index 154 / font 14 / loaders PASS)。shelf_bench_ondev 增加 index_load/scan_save 计时与 MEOW_BENCH_IDX=miss(真机首开命中 vs 未命中对比,待有卡复核)。 交付:app.json 0.1.4/code5 → 0.1.5/code6;reader md5 df4cadcf 1,711,924B;mpk md5 fec7b1bb 814,535B;未签名不推卡。
102 lines
6.1 KiB
C
102 lines
6.1 KiB
C
/* app/shelf_model.h — 书架数据模型:扫描 Books/ + 生成行序列(纯 C,不依赖 SDK/设备)。
|
||
*
|
||
* 从 shelf.c 拆出,供 shelf.c(控件装配)、宿主单测(test/shelf_group_smoke.c)与
|
||
* 扫描计时(test/shelf_scan_bench.c)共用。
|
||
*
|
||
* 目录归属(M27 r2):folder 模式按「Books/ 下的第一级目录」分组(如 "轻小说"、
|
||
* "400+本高质量完本合集");更深的相对子路径("01、玄幻/01、斗破")进该书的副题,
|
||
* 不再每组一本书把书架炸成上百个段头。Books/ 根下的书 top="",显示段头 SHELF_GROUP_ROOT。
|
||
* 分组键是「第一级目录名」,完整相对路径保留在 group[] 供副题/调试用。
|
||
* a/b 与 a/c 同属 a 组;a/b 与 b 仍是两个组(不会因为只取末级目录名而混)。
|
||
*
|
||
* 硬约束:path 永远是书的完整路径(打开书 + 进度键 rdprog_hash(path)),分组只写 top/group,
|
||
* 绝不改写 path;两个分组模式间切换不影响进度。
|
||
*/
|
||
#ifndef SHELF_MODEL_H
|
||
#define SHELF_MODEL_H
|
||
|
||
#include <stdint.h>
|
||
|
||
/* 书籍上限:真机真实书库 646 文件/约 595 本候选,2048 给 3×+ 余量。
|
||
* 超限不再静默停扫:report->truncated=1,由 UI 出提示。 */
|
||
#define SHELF_MAX_BOOKS 2048
|
||
#define SHELF_GROUP_MAX 96 /* 目录字段容量(含 NUL):95 字节 ≈ 31 个汉字 */
|
||
/* 扫描深度上限(root=0;文件所在目录 depth ≤ 该值才会被收录)。
|
||
* 真机真实布局 Books/400+本高质量完本合集/<分类>/<作品>/<书名>.txt 最深 4 层,取 6 留余量。
|
||
* 超过上限的书籍文件计入 report->depth_skipped(不静默跳过)。 */
|
||
#define SHELF_SCAN_MAX_DEPTH 6
|
||
/* 行数上限:每本书最多贡献 1 行书 + 1 行段头(段头数 ≤ 书数),故 2N 必够。 */
|
||
#define SHELF_MAX_ROWS (SHELF_MAX_BOOKS * 2)
|
||
#define SHELF_GROUP_ROOT "未分类" /* Books/ 根下书籍的段头 */
|
||
|
||
typedef struct shelf_book {
|
||
char path[400]; /* 完整路径:打开书 + 进度键;分组不得改写 */
|
||
char title[128];
|
||
char sub[160]; /* 默认(format/宫格)副题:格式 · 大小 [· 读],与 M27 前逐字一致 */
|
||
char sub_folder[160]; /* folder 模式副题:更深子路径 · 格式 · 大小 [· 读] */
|
||
char group[SHELF_GROUP_MAX]; /* 相对 Books/ 的完整目录路径(供副题);根下="" */
|
||
char top[SHELF_GROUP_MAX]; /* 分组键:相对路径的第一级目录名;根下="" */
|
||
int fmt; /* 0 txt,1 fb2,2 epub,3 mobi,4 cbz */
|
||
long size;
|
||
uint64_t ts; /* 最近阅读 unix;无=0 */
|
||
} shelf_book;
|
||
|
||
typedef struct shelf_row {
|
||
int section; /* 1=段头行 */
|
||
int fmt; /* 段头/书行格式;段头无 label 时显示格式名 */
|
||
int book; /* 书行=books 下标;段头=-1 */
|
||
const char *label; /* 段头标题;NULL=格式名(指向 books[].top 或静态串) */
|
||
} shelf_row;
|
||
|
||
/* 进度时间戳查询(生产=rdprog_load;测试/基线=NULL,全部 ts=0)。 */
|
||
typedef uint64_t (*shelf_ts_fn)(const char *path);
|
||
|
||
/* M32:扫描时逐目录回调(书架索引的失效校验数据源)。
|
||
* rel = 相对 root 的目录路径(root 自身为 ""),books_here = 该目录里**实际收录**的
|
||
* 书籍文件数,mtime = 该目录的 stat 结果(秒 + 纳秒)。
|
||
* 回调**不能中止扫描**(返回 void):索引写盘失败只应放弃索引,书架必须照常出书。 */
|
||
typedef void (*shelf_dir_fn)(void *ctx, const char *rel, int64_t mtime_sec,
|
||
long mtime_nsec, int books_here);
|
||
|
||
/* 最近阅读排序:ts 降序,同 ts 按书名(不区分大小写)。扫描后与「刚读完置顶」共用。 */
|
||
void shelf_sort_recent(shelf_book *books, int nbooks);
|
||
|
||
typedef struct shelf_scan_report {
|
||
int nbooks;
|
||
int truncated; /* 1=有书因 SHELF_MAX_BOOKS 上限被丢弃 */
|
||
int depth_skipped; /* 因目录深度 > SHELF_SCAN_MAX_DEPTH 未收录的书籍文件数 */
|
||
int root_missing; /* 1=root 打不开(无卡/无 Books 目录) */
|
||
} shelf_scan_report;
|
||
|
||
/* 递归扫描 root(目录深度 ≤ SHELF_SCAN_MAX_DEPTH,超限文件计数但不出书),
|
||
* 按 ts 降序/书名升序排序后写 books(容量 cap)。
|
||
* 返回写入本数。空目录不产生任何记录(段头由 build_rows 按书生成,天然无空段头)。
|
||
* dir_fn 非 NULL 时,每进入并成功扫描一个目录回调一次(见 shelf_dir_fn)。 */
|
||
int shelf_scan_books_ex(const char *root, shelf_book *books, int cap,
|
||
shelf_ts_fn ts_fn, shelf_scan_report *rep,
|
||
shelf_dir_fn dir_fn, void *dir_ctx);
|
||
|
||
/* 同 shelf_scan_books_ex(不采集目录 mtime),供旧调用方/基准复用。 */
|
||
int shelf_scan_books(const char *root, shelf_book *books, int cap,
|
||
shelf_ts_fn ts_fn, shelf_scan_report *rep);
|
||
|
||
/* 生成行序列:
|
||
* folder_mode=0:按格式分段(TXT/FB2/EPUB/MOBI/CBZ,空格式不出段头)——与 M27 前逐行一致;
|
||
* folder_mode=1:按 books[].top(Books/ 第一级目录)分组,根组「未分类」置顶,
|
||
* 其余组按组名排序;组内沿用 books[] 全局顺序(最近阅读优先、其次书名)。
|
||
* 返回行数;超过 rowcap 时只写 rowcap 行并返回 rowcap。 */
|
||
int shelf_build_rows(const shelf_book *books, int nbooks, int folder_mode,
|
||
shelf_row *rows, int rowcap);
|
||
|
||
/* 工具(单测直接调用)。 */
|
||
int shelf_fmt_from_ext(const char *name);
|
||
const char *shelf_fmt_name(int fmt);
|
||
void shelf_fmt_size(char *out, int outsz, long size);
|
||
/* 按 fmt/size/ts/group 重建 b->sub 与 b->sub_folder(扫描后与「刚读完置顶」共用)。 */
|
||
void shelf_fmt_sub(shelf_book *b);
|
||
/* UTF-8 安全截断:最多写 cap-1 字节;若截断点落在多字节序列中间,整段丢弃该残缺字符。
|
||
* 返回写入字节数(不含 NUL)。 */
|
||
int shelf_copy_utf8(char *dst, int cap, const char *src);
|
||
|
||
#endif /* SHELF_MODEL_H */
|