r1 用宿主 1.1µs/stat + M28 的**热** stat 150µs 外推「434 次≈65ms」,真机 drop_caches 后 实测是 3.15s(7.26ms/次),与 readdir 的 3.0s 同量级:全目录校验的索引命中冷启动仍要 3.06s,只比全扫快 0.2s。「3.0s→0.15s」在冷启动不成立。按任务 1 自己定的判据 (stat 接近 1s 就退回 level-1)改为只校验 Books 根 + 每个一级目录。 真机冷缓存实测(exFAT /usr/data/mnt/sd_0,434 目录/646 文件/595 本;sync + drop_caches; 探针走 /proc/uptime,UI 走 meow_ui_time_ms): - 索引命中首开:**127 / 138 / 157ms**(复测 147ms),热 dentry 107–120ms; - 全量扫描首开:**3428 / 3477ms**(复测 3711ms),热 dentry 不变(readdir 不吃 page cache); - 探针冷分项:索引载入 20–30ms;载入+rdprog×595 70–120ms;4 个目录 stat 0–30ms; - 从阅读页返回书架 29–31ms(M29 语义未变)。 → 冷启动 22–26 倍提升,方案值得做;索引实测 **63,579B**(v2;r1 全目录 97,490B)。 实现(格式 v2,magic MEOWIDX2 / version 2): - writer 回调过滤 rel 含 '/' 的目录:只落 root + 一级目录的 mtime;书/大小/格式记录仍全量; level-2+ 不落盘、载入不 stat。载入仍然 0 次 opendir/readdir(strace:0 getdents64, 真实库 4 次 stat;宿主模拟库 11 次)。 - 校验记录里出现 level-2+ rel 视为坏档;sum(books_here) 改为「≤ nbooks」合理性上界。 - fail-open/原子写/CRC/安全加固/ts 不缓存全部保留(r1 已过评审的部分未动)。 失效矩阵(报文逐条写清): 自动(根 mtime):Books 根下直接增/删/改名书;一级目录新增/删除/改名。 自动(一级目录 mtime):一级目录里直接增/删/改名书、新建子目录。 必须刷新:level-2 及更深处的任何变化(只改深层目录 mtime)。刷新=删索引+全扫。 真机 exFAT mtime 实测 **1 秒**(tm_nsec=0;同秒连续增删 mtime 不变)。我复测 10 轮 「目录刚改过就扫描、同秒再改」→ 9/10 仍命中(陈旧),与评审 29/30 一致;sleep 跨秒后 全部正常失效。头注释已把「10ms」改为「1 秒」,并写明「不在同一秒刻度的变更靠索引失效, 同秒二次变更需刷新」。 bench 修复(P3-3):MEOW_BENCH_IDX=only —— **不预先 load/scan**,直接进书架量生产首开; miss —— 先删索引再进书架。两者配合 drop_caches 即冷启动口径。旧行为(先 bench_index/ bench_scan 焐热)保留为默认模式。新增 test/shelf_index_probe_mipsel(write/scan/load/ loadts/statpaths/mtime/size)用于真机分项与 mtime 粒度验证,不进发行包。 测试:shelf_index_smoke 170 pass(新增:level-3/level-2 内变更「索引仍命中且先看到旧 列表 → 刷新后可见」、新建 level-2 目录自动失效、一级目录直接删书自动失效、v2 ndirs= 根+一级数);ASan+UBSan 干净;build_reader.sh 0 warning,core 6846 / seg 19 / chapter 71 / shelf_group 63 / index 170 / font 14 / loaders PASS。真机 6 场景逐项验证:一级直接增/ 新建子目录/一级改名/根下删书/一级直接删书 = 自动失效;level-3 深处新增 = 仍命中(需刷新)。 交付:app.json 0.1.5/code6 → **0.1.6/code7**;reader md5 5c848c99… 1,711,988B; mpk md5 10d676f5… 814,574B;未签名不 push。设备验证后已清理 /tmp 与 m32probe, 真实 MeowData 的 shelf.idx 已删(app 自己的章节 .idx 未动),Books 434/646 不变, 已装 0.1.4 md5 4036b889… 未变。
509 lines
19 KiB
C
509 lines
19 KiB
C
/* app/shelf_index.c — 书架索引落盘/校验(实现见 app/shelf_index.h)。
|
||
*
|
||
* 落盘格式 v2(全部小端、手工序列化,不依赖结构体布局):
|
||
* [header 24B] "MEOWIDX2" | u32 version | u32 header_size | u32 root_len | u32 rsv
|
||
* [root bytes]
|
||
* [dir 记录 ×N] u32 rel_len | u32 books_here | u64 mtime_sec | u32 mtime_nsec | u32 rsv | rel
|
||
* 只含 Books 根(rel="")与一级目录(rel 不含 '/');level-2+ 不落盘。
|
||
* [book 记录 ×M] u32 rel_len | u32 size | u32 fmt | u32 rsv | rel
|
||
* [trailer 24B] u32 ndirs | u32 nbooks | u32 flags | u32 depth_skipped | u32 crc32 | u32 magic
|
||
* crc32 覆盖 [0, trailer_off)(含 header/root/全部记录),用来发现截断/坏页。
|
||
* rel 一律相对 Books 根,路径做 ".."/绝对路径/内嵌 NUL 校验,杜绝索引坏档造成越界读。
|
||
*
|
||
* 校验范围只有根 + 一级目录(真机冷缓存 434 次 stat=3.15s,4 次≈20ms;见头文件)。
|
||
* level-2 及更深处的变更不会自动失效,需点「刷新」——这是本方案明确接受的代价。
|
||
*
|
||
* 写入顺序:临时文件 `shelf.idx.tmp` 全量写完(flash 尽力 fsync)后 rename 发布。
|
||
* 扫描中途被杀只会留下 tmp(下次覆盖),正式索引要么是旧的、要么是完整新的。
|
||
*/
|
||
#include "shelf_index.h"
|
||
#include "meow_ui.h" /* meow_config_dir() */
|
||
|
||
#include <fcntl.h>
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/types.h>
|
||
#include <unistd.h>
|
||
|
||
#define IDX_HDR_SIZE 24
|
||
#define IDX_TRL_SIZE 24
|
||
#define IDX_TRL_MAGIC 0x314D5749u /* "IWM1" */
|
||
#define IDX_REL_MAX 1000 /* 单条 rel 上限(真机 <100B,留足余量) */
|
||
#define IDX_DIR_REC_FIX 24 /* dir 记录定长部分 */
|
||
#define IDX_BOOK_REC_FIX 16 /* book 记录定长部分 */
|
||
#define IDX_MAX_SIZE (8 * 1024 * 1024)
|
||
|
||
static const unsigned char IDX_MAGIC[8] = { 'M','E','O','W','I','D','X','2' };
|
||
|
||
/* ---- 小端序列化 ---- */
|
||
static void put32(unsigned char *p, uint32_t v)
|
||
{
|
||
p[0] = (unsigned char)(v & 0xFF); p[1] = (unsigned char)((v >> 8) & 0xFF);
|
||
p[2] = (unsigned char)((v >> 16) & 0xFF); p[3] = (unsigned char)((v >> 24) & 0xFF);
|
||
}
|
||
static uint32_t get32(const unsigned char *p)
|
||
{
|
||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||
}
|
||
static void put64(unsigned char *p, uint64_t v)
|
||
{
|
||
put32(p, (uint32_t)(v & 0xFFFFFFFFu));
|
||
put32(p + 4, (uint32_t)((v >> 32) & 0xFFFFFFFFu));
|
||
}
|
||
static uint64_t get64(const unsigned char *p)
|
||
{
|
||
return (uint64_t)get32(p) | ((uint64_t)get32(p + 4) << 32);
|
||
}
|
||
|
||
/* ---- CRC32(IEEE 802.3,查表;表首次使用时生成 1KB) ---- */
|
||
static uint32_t g_crc_tbl[256];
|
||
static int g_crc_ready;
|
||
|
||
static void crc_table_init(void)
|
||
{
|
||
for (uint32_t i = 0; i < 256; i++) {
|
||
uint32_t c = i;
|
||
for (int k = 0; k < 8; k++)
|
||
c = (c & 1) ? (0xEDB88320u ^ (c >> 1)) : (c >> 1);
|
||
g_crc_tbl[i] = c;
|
||
}
|
||
g_crc_ready = 1;
|
||
}
|
||
static uint32_t crc_step(uint32_t c, const void *buf, size_t n)
|
||
{
|
||
if (!g_crc_ready) crc_table_init();
|
||
const unsigned char *p = (const unsigned char *)buf;
|
||
while (n--) c = g_crc_tbl[(c ^ *p++) & 0xFF] ^ (c >> 8);
|
||
return c;
|
||
}
|
||
|
||
/* ---- 路径推导:<MeowData>/<pkg>/data → <MeowData>/<pkg>/index/shelf.idx ---- */
|
||
static void strip_data_suffix(const char *d, char *out, int outsz)
|
||
{
|
||
if (!d || !d[0]) { strncpy(out, "/tmp", (size_t)outsz - 1); out[outsz - 1] = 0; return; }
|
||
size_t n = strlen(d);
|
||
const char *suffix = "/data";
|
||
size_t sl = strlen(suffix);
|
||
size_t keep = (n >= sl && strcmp(d + n - sl, suffix) == 0) ? n - sl : n;
|
||
if (keep >= (size_t)outsz) keep = (size_t)outsz - 1;
|
||
memcpy(out, d, keep);
|
||
out[keep] = 0;
|
||
}
|
||
|
||
/* mkdir -p(逐分量,EEXIST 视为成功)。 */
|
||
static int mkdir_p(const char *path)
|
||
{
|
||
char tmp[520];
|
||
if (!path || !path[0] || strlen(path) >= sizeof tmp) return -1;
|
||
memcpy(tmp, path, strlen(path) + 1);
|
||
for (char *p = tmp + 1; *p; p++) {
|
||
if (*p != '/') continue;
|
||
*p = 0; mkdir(tmp, 0755); *p = '/';
|
||
}
|
||
mkdir(tmp, 0755);
|
||
return 0;
|
||
}
|
||
|
||
/* 索引目录 <MeowData>/<pkg>/index(只算路径;mkdir 只在写索引时做)。 */
|
||
static int index_dir(char *out, int outsz)
|
||
{
|
||
const char *data = meow_config_dir();
|
||
if (!data) return -2;
|
||
char base[400];
|
||
strip_data_suffix(data, base, sizeof base);
|
||
if (snprintf(out, (size_t)outsz, "%s/index", base) >= outsz) return -3;
|
||
return 0;
|
||
}
|
||
|
||
int shelf_index_path(const char *root, char *out, int outsz)
|
||
{
|
||
(void)root; /* 索引跟 App 数据目录走,不跟 Books 路径走 */
|
||
if (!out || outsz <= 0) return -1;
|
||
out[0] = 0;
|
||
char dir[440];
|
||
if (index_dir(dir, sizeof dir) != 0) return -2;
|
||
if (snprintf(out, (size_t)outsz, "%s/shelf.idx", dir) >= outsz) return -5;
|
||
return 0;
|
||
}
|
||
|
||
/* ---- rel 校验:相对路径、无 ".." 分量、无内嵌 NUL、长度受控 ---- */
|
||
static int rel_valid(const char *rel, int len, int allow_empty)
|
||
{
|
||
if (len < 0 || len > IDX_REL_MAX) return 0;
|
||
if (len == 0) return allow_empty;
|
||
if (rel[0] == '/') return 0;
|
||
if (memchr(rel, 0, (size_t)len) != NULL) return 0;
|
||
int comp = 0;
|
||
for (int i = 0; i <= len; i++) {
|
||
char ch = (i == len) ? '/' : rel[i];
|
||
if (ch == '/') {
|
||
if (i - comp == 2 && rel[comp] == '.' && rel[comp + 1] == '.') return 0;
|
||
comp = i + 1;
|
||
}
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
/* 目录 rel(""/a/b)拼成绝对路径;返回 0=成功。 */
|
||
static int join_path(const char *root, const char *rel, int rel_len, char *out, int outsz)
|
||
{
|
||
if (rel_len == 0) {
|
||
if ((int)strlen(root) >= outsz) return -1;
|
||
memcpy(out, root, strlen(root) + 1);
|
||
return 0;
|
||
}
|
||
int n = snprintf(out, (size_t)outsz, "%s/%s", root, rel);
|
||
return (n < 0 || n >= outsz) ? -1 : 0;
|
||
}
|
||
|
||
/* book rel → title/group/top。与 shelf_scan_books 的写法保持逐字一致
|
||
* (path 必须是同一个绝对路径,进度键 rdprog_hash(path) 才能对上)。 */
|
||
static void fill_from_rel(shelf_book *b, const char *rel, int rel_len)
|
||
{
|
||
const char *base = rel;
|
||
for (int i = 0; i < rel_len; i++) if (rel[i] == '/') base = rel + i + 1;
|
||
char name[256];
|
||
int nl = rel_len - (int)(base - rel);
|
||
if (nl >= (int)sizeof name) nl = (int)sizeof name - 1;
|
||
memcpy(name, base, (size_t)nl);
|
||
name[nl] = 0;
|
||
shelf_copy_utf8(b->title, (int)sizeof b->title, name);
|
||
char *dot = strrchr(b->title, '.');
|
||
if (dot) *dot = 0;
|
||
|
||
/* group = rel 去掉最后一段;top = 第一段。*/
|
||
int glen = (int)(base - rel);
|
||
if (glen > 0) glen--; /* 去掉结尾 '/' */
|
||
char g[IDX_REL_MAX + 1];
|
||
if (glen < 0) glen = 0;
|
||
memcpy(g, rel, (size_t)glen);
|
||
g[glen] = 0;
|
||
shelf_copy_utf8(b->group, (int)sizeof b->group, g);
|
||
const char *sl = strchr(g, '/');
|
||
int tlen = sl ? (int)(sl - g) : glen;
|
||
char t[IDX_REL_MAX + 1];
|
||
memcpy(t, g, (size_t)tlen);
|
||
t[tlen] = 0;
|
||
shelf_copy_utf8(b->top, (int)sizeof b->top, t);
|
||
}
|
||
|
||
/* ================= 载入 ================= */
|
||
|
||
int shelf_index_load(const char *root, shelf_book *books, int cap,
|
||
shelf_ts_fn ts_fn, shelf_scan_report *rep)
|
||
{
|
||
if (rep) { rep->nbooks = 0; rep->truncated = 0; rep->depth_skipped = 0; rep->root_missing = 0; }
|
||
if (!root || !books || cap <= 0) return 0;
|
||
|
||
char ipath[500];
|
||
if (shelf_index_path(root, ipath, sizeof ipath) != 0) return 0;
|
||
/* O_NOFOLLOW/O_NONBLOCK + S_ISREG:索引是 App 自己的普通文件;卡上若有同名 FIFO/符号链接,
|
||
* 直接拒绝重扫,避免 fopen 阻塞或读到卡外内容。 */
|
||
int fd = open(ipath, O_RDONLY | O_NONBLOCK | O_NOFOLLOW);
|
||
if (fd < 0) return 0;
|
||
struct stat fst;
|
||
if (fstat(fd, &fst) != 0 || !S_ISREG(fst.st_mode) || fst.st_size <= 0) {
|
||
close(fd);
|
||
return 0;
|
||
}
|
||
FILE *f = fdopen(fd, "rb");
|
||
if (!f) { close(fd); return 0; }
|
||
|
||
int ok = 0;
|
||
long fsz = (long)fst.st_size;
|
||
if (fsz < IDX_HDR_SIZE + IDX_TRL_SIZE || fsz > IDX_MAX_SIZE) goto done;
|
||
|
||
/* trailer 先读:拿 ndirs/nbooks/crc,再从头顺序校验。 */
|
||
unsigned char tr[IDX_TRL_SIZE];
|
||
if (fseek(f, fsz - IDX_TRL_SIZE, SEEK_SET) != 0) goto done;
|
||
if (fread(tr, 1, sizeof tr, f) != sizeof tr) goto done;
|
||
if (get32(tr + 20) != IDX_TRL_MAGIC) goto done;
|
||
uint32_t ndirs = get32(tr + 0);
|
||
uint32_t nbooks = get32(tr + 4);
|
||
uint32_t flags = get32(tr + 8);
|
||
uint32_t depth_skipped = get32(tr + 12);
|
||
uint32_t crc_expect = get32(tr + 16);
|
||
if (ndirs < 1 || ndirs > 65535) goto done;
|
||
if (nbooks > (uint32_t)cap || nbooks > (uint32_t)SHELF_MAX_BOOKS) goto done;
|
||
if (depth_skipped > 0x7FFFFFFFu) goto done;
|
||
|
||
long trailer_off = fsz - IDX_TRL_SIZE;
|
||
if (fseek(f, 0, SEEK_SET) != 0) goto done;
|
||
uint32_t crc = 0xFFFFFFFFu;
|
||
|
||
/* header + root */
|
||
unsigned char h[IDX_HDR_SIZE];
|
||
if (fread(h, 1, sizeof h, f) != sizeof h) goto done;
|
||
crc = crc_step(crc, h, sizeof h);
|
||
if (memcmp(h, IDX_MAGIC, 8) != 0) goto done;
|
||
if (get32(h + 8) != SHELF_INDEX_VERSION) goto done;
|
||
if (get32(h + 12) != IDX_HDR_SIZE) goto done;
|
||
uint32_t root_len = get32(h + 16);
|
||
if (root_len > IDX_REL_MAX || (int)root_len != (int)strlen(root)) goto done;
|
||
char rootbuf[IDX_REL_MAX + 1];
|
||
if (root_len > 0 && fread(rootbuf, 1, root_len, f) != root_len) goto done;
|
||
if (root_len > 0) crc = crc_step(crc, rootbuf, root_len);
|
||
rootbuf[root_len] = 0;
|
||
if (strcmp(rootbuf, root) != 0) goto done; /* 换了卡/挂载点 → 重扫 */
|
||
|
||
/* 校验记录:只有 Books 根 + 一级目录(v2)。逐个 stat 比对 mtime(唯一允许的校验 IO,
|
||
* 不做 readdir)。level-2+ 的 rel 含 '/',出现在 v2 文件里即视为坏档。 */
|
||
uint32_t sum_here = 0;
|
||
for (uint32_t i = 0; i < ndirs; i++) {
|
||
unsigned char rh[IDX_DIR_REC_FIX];
|
||
if (fread(rh, 1, sizeof rh, f) != sizeof rh) goto done;
|
||
crc = crc_step(crc, rh, sizeof rh);
|
||
uint32_t rel_len = get32(rh + 0);
|
||
uint32_t books_here = get32(rh + 4);
|
||
uint64_t mt_sec = get64(rh + 8);
|
||
uint32_t mt_nsec = get32(rh + 16);
|
||
if (rel_len > IDX_REL_MAX || books_here > (uint32_t)cap) goto done;
|
||
char rel[IDX_REL_MAX + 1];
|
||
if (rel_len > 0 && fread(rel, 1, rel_len, f) != rel_len) goto done;
|
||
if (rel_len > 0) crc = crc_step(crc, rel, rel_len);
|
||
rel[rel_len] = 0;
|
||
if (!rel_valid(rel, (int)rel_len, 1)) goto done;
|
||
if (rel_len > 0 && strchr(rel, '/')) goto done; /* v2 只允许一级 */
|
||
|
||
char path[400];
|
||
if (join_path(root, rel, (int)rel_len, path, sizeof path) != 0) goto done;
|
||
struct stat st;
|
||
if (stat(path, &st) != 0) goto done;
|
||
if (!S_ISDIR(st.st_mode)) goto done;
|
||
if ((uint64_t)(int64_t)st.st_mtime != mt_sec) goto done;
|
||
if ((uint32_t)(long)st.st_mtim.tv_nsec != mt_nsec) goto done;
|
||
sum_here += books_here; /* 只作合理性上界:根+一级里直接放的书写数 ≤ nbooks */
|
||
}
|
||
|
||
/* 书记录 */
|
||
for (uint32_t i = 0; i < nbooks; i++) {
|
||
unsigned char bh[IDX_BOOK_REC_FIX];
|
||
if (fread(bh, 1, sizeof bh, f) != sizeof bh) goto done;
|
||
crc = crc_step(crc, bh, sizeof bh);
|
||
uint32_t rel_len = get32(bh + 0);
|
||
uint32_t size = get32(bh + 4);
|
||
uint32_t fmt = get32(bh + 8);
|
||
if (rel_len < 1 || rel_len > IDX_REL_MAX || fmt > 4) goto done;
|
||
char rel[IDX_REL_MAX + 1];
|
||
if (fread(rel, 1, rel_len, f) != rel_len) goto done;
|
||
crc = crc_step(crc, rel, rel_len);
|
||
rel[rel_len] = 0;
|
||
if (!rel_valid(rel, (int)rel_len, 0)) goto done;
|
||
|
||
shelf_book *b = &books[i];
|
||
memset(b, 0, sizeof *b);
|
||
if (join_path(root, rel, (int)rel_len, b->path, sizeof b->path) != 0) goto done;
|
||
if ((int)strlen(b->path) != (int)(strlen(root) + 1 + rel_len)) goto done;
|
||
fill_from_rel(b, rel, (int)rel_len);
|
||
b->fmt = (int)fmt;
|
||
b->size = (long)size;
|
||
b->ts = 0; /* 进度不落盘:下面用 ts_fn 现读 */
|
||
}
|
||
|
||
/* 必须正好读到 trailer 起点(多/少字节都视为坏档) */
|
||
if (ftell(f) != trailer_off) goto done;
|
||
if ((crc ^ 0xFFFFFFFFu) != crc_expect) goto done;
|
||
/* v2 只校验根+一级:books_here 之和是「根/一级里直接放的书写数」,不可能超过总书数。 */
|
||
if (sum_here > nbooks) goto done;
|
||
|
||
/* 校验全过:现读进度 ts(与全量扫描同一口径),重建副题并按最近阅读排序。 */
|
||
for (uint32_t i = 0; i < nbooks; i++) {
|
||
books[i].ts = ts_fn ? ts_fn(books[i].path) : 0;
|
||
shelf_fmt_sub(&books[i]);
|
||
}
|
||
shelf_sort_recent(books, (int)nbooks);
|
||
|
||
if (rep) {
|
||
rep->nbooks = (int)nbooks;
|
||
rep->truncated = (flags & 1u) ? 1 : 0;
|
||
rep->depth_skipped = (int)depth_skipped;
|
||
rep->root_missing = 0;
|
||
}
|
||
ok = 1;
|
||
|
||
done:
|
||
fclose(f);
|
||
return ok;
|
||
}
|
||
|
||
/* ================= 写入 ================= */
|
||
|
||
struct shelf_idx_writer {
|
||
FILE *f;
|
||
char root[400];
|
||
char dst[500];
|
||
char tmp[520];
|
||
uint32_t crc;
|
||
int ndirs;
|
||
int ok;
|
||
};
|
||
|
||
static int wraw(shelf_idx_writer *w, const void *buf, size_t n)
|
||
{
|
||
if (!w->ok) return -1;
|
||
if (fwrite(buf, 1, n, w->f) != n) { w->ok = 0; return -1; }
|
||
w->crc = crc_step(w->crc, buf, n);
|
||
return 0;
|
||
}
|
||
|
||
shelf_idx_writer *shelf_index_writer_open(const char *root)
|
||
{
|
||
if (!root) return NULL;
|
||
size_t rl = strlen(root);
|
||
if (rl == 0 || rl >= sizeof(((shelf_idx_writer *)0)->root)) return NULL;
|
||
|
||
shelf_idx_writer *w = (shelf_idx_writer *)calloc(1, sizeof *w);
|
||
if (!w) return NULL;
|
||
memcpy(w->root, root, rl + 1);
|
||
|
||
if (shelf_index_path(root, w->dst, sizeof w->dst) != 0) { free(w); return NULL; }
|
||
char idir[500];
|
||
snprintf(idir, sizeof idir, "%s", w->dst);
|
||
char *slash = strrchr(idir, '/');
|
||
if (!slash) { free(w); return NULL; }
|
||
*slash = 0;
|
||
mkdir_p(idir); /* 失败也让 fopen 去失败(只读卡 → 不写索引,书架照常) */
|
||
if (snprintf(w->tmp, sizeof w->tmp, "%s.tmp", w->dst) >= (int)sizeof w->tmp) { free(w); return NULL; }
|
||
|
||
w->f = NULL;
|
||
int fd = open(w->tmp, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0644);
|
||
if (fd < 0) { free(w); return NULL; }
|
||
w->f = fdopen(fd, "wb");
|
||
if (!w->f) { close(fd); unlink(w->tmp); free(w); return NULL; }
|
||
w->ok = 1;
|
||
w->crc = 0xFFFFFFFFu;
|
||
|
||
unsigned char h[IDX_HDR_SIZE];
|
||
memset(h, 0, sizeof h);
|
||
memcpy(h, IDX_MAGIC, 8);
|
||
put32(h + 8, SHELF_INDEX_VERSION);
|
||
put32(h + 12, IDX_HDR_SIZE);
|
||
put32(h + 16, (uint32_t)rl);
|
||
if (wraw(w, h, sizeof h) != 0 || wraw(w, root, rl) != 0) {
|
||
shelf_index_writer_abort(w);
|
||
return NULL;
|
||
}
|
||
return w;
|
||
}
|
||
|
||
void shelf_index_writer_dir(void *ctx, const char *rel, int64_t mtime_sec,
|
||
long mtime_nsec, int books_here)
|
||
{
|
||
shelf_idx_writer *w = (shelf_idx_writer *)ctx;
|
||
if (!w || !w->ok || !rel || books_here < 0) return;
|
||
/* v2:只落 Books 根("")与一级目录(rel 不含 '/')——真机冷缓存下 434 次 stat=3.15s,
|
||
* 4 次≈20ms;level-2+ 的 mtime 不参与校验(深层变更靠「刷新」,见头文件)。 */
|
||
if (rel[0] && strchr(rel, '/')) return;
|
||
size_t rl = strlen(rel);
|
||
if (rl > IDX_REL_MAX) { w->ok = 0; return; }
|
||
|
||
unsigned char rh[IDX_DIR_REC_FIX];
|
||
put32(rh + 0, (uint32_t)rl);
|
||
put32(rh + 4, (uint32_t)books_here);
|
||
put64(rh + 8, (uint64_t)mtime_sec);
|
||
put32(rh + 16, (uint32_t)mtime_nsec);
|
||
put32(rh + 20, 0);
|
||
if (wraw(w, rh, sizeof rh) != 0) return;
|
||
if (rl > 0 && wraw(w, rel, rl) != 0) return;
|
||
w->ndirs++;
|
||
}
|
||
|
||
int shelf_index_writer_commit(shelf_idx_writer *w, const shelf_book *books,
|
||
int nbooks, const shelf_scan_report *rep)
|
||
{
|
||
if (!w) return 0;
|
||
if (!w->ok || nbooks < 0 || (nbooks > 0 && !books) ||
|
||
w->ndirs <= 0 || w->ndirs > 65535) {
|
||
shelf_index_writer_abort(w);
|
||
return 0;
|
||
}
|
||
|
||
size_t root_len = strlen(w->root);
|
||
for (int i = 0; i < nbooks; i++) {
|
||
const shelf_book *b = &books[i];
|
||
size_t pl = strlen(b->path);
|
||
if (pl <= root_len + 1 || strncmp(b->path, w->root, root_len) != 0 ||
|
||
b->path[root_len] != '/') {
|
||
/* 路径不是扫描 root 下的相对路径(被截断/异常)→ 宁可不写索引 */
|
||
shelf_index_writer_abort(w);
|
||
return 0;
|
||
}
|
||
const char *rel = b->path + root_len + 1;
|
||
size_t rl = strlen(rel);
|
||
if (rl < 1 || rl > IDX_REL_MAX || b->fmt < 0 || b->fmt > 4 ||
|
||
b->size < 0 || (uint64_t)b->size > 0xFFFFFFFFu) {
|
||
shelf_index_writer_abort(w);
|
||
return 0;
|
||
}
|
||
unsigned char bh[IDX_BOOK_REC_FIX];
|
||
put32(bh + 0, (uint32_t)rl);
|
||
put32(bh + 4, (uint32_t)b->size);
|
||
put32(bh + 8, (uint32_t)b->fmt);
|
||
put32(bh + 12, 0);
|
||
if (wraw(w, bh, sizeof bh) != 0 || wraw(w, rel, rl) != 0) {
|
||
shelf_index_writer_abort(w);
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
uint32_t flags = 0;
|
||
int depth = 0;
|
||
if (rep) {
|
||
if (rep->truncated) flags |= 1u;
|
||
if (rep->depth_skipped > 0) { flags |= 2u; depth = rep->depth_skipped; }
|
||
}
|
||
unsigned char tr[IDX_TRL_SIZE];
|
||
put32(tr + 0, (uint32_t)w->ndirs);
|
||
put32(tr + 4, (uint32_t)nbooks);
|
||
put32(tr + 8, flags);
|
||
put32(tr + 12, (uint32_t)depth);
|
||
put32(tr + 16, w->crc ^ 0xFFFFFFFFu);
|
||
put32(tr + 20, IDX_TRL_MAGIC);
|
||
if (wraw(w, tr, sizeof tr) != 0) {
|
||
shelf_index_writer_abort(w);
|
||
return 0;
|
||
}
|
||
|
||
/* 刷盘后再 rename:断电最多丢 tmp,正式索引保持旧版本(仍受 mtime 校验保护)。
|
||
* fsync 失败不算致命(FUSE/部分卡返回 EINVAL):rename 后若内容不完整,CRC 会拒绝。 */
|
||
if (fflush(w->f) != 0) {
|
||
shelf_index_writer_abort(w);
|
||
return 0;
|
||
}
|
||
(void)fsync(fileno(w->f));
|
||
if (fclose(w->f) != 0) {
|
||
w->f = NULL;
|
||
unlink(w->tmp);
|
||
free(w);
|
||
return 0;
|
||
}
|
||
w->f = NULL;
|
||
if (rename(w->tmp, w->dst) != 0) {
|
||
/* exFAT 某些实现不支持「rename 覆盖已存在文件」:先删旧再改名。 */
|
||
unlink(w->dst);
|
||
if (rename(w->tmp, w->dst) != 0) {
|
||
unlink(w->tmp);
|
||
free(w);
|
||
return 0;
|
||
}
|
||
}
|
||
free(w);
|
||
return 1;
|
||
}
|
||
|
||
void shelf_index_writer_abort(shelf_idx_writer *w)
|
||
{
|
||
if (!w) return;
|
||
if (w->f) fclose(w->f);
|
||
if (w->tmp[0]) unlink(w->tmp);
|
||
free(w);
|
||
}
|
||
|
||
void shelf_index_remove(const char *root)
|
||
{
|
||
char ipath[500];
|
||
if (shelf_index_path(root, ipath, sizeof ipath) == 0) unlink(ipath);
|
||
}
|