Files
MeowOS-For-R3P2/patch_v2/reader_app/app/shelf_index.c
T
AskaEth 58f2d9bff4 perf(reader): M32 r2 书架索引改 level-1 校验 — 真机冷缓存首开 3.47s→0.13–0.16s(r1 全目录校验被真机证伪)
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… 未变。
2026-11-10 18:49:48 +08:00

509 lines
19 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.
/* 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);
}