perf(reader): M32 书架索引落盘 — 冷启动只 stat 434 个目录 mtime,跳过 readdir(3.0s→~0.15s)

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;未签名不推卡。
This commit is contained in:
AskaEth committed 2026-11-10 18:14:52 +08:00
1 parent 3f09f7c137
commit e0dfab95c3
11 files changed
+1388 -29

No files matched your search

+498
View File
@@ -0,0 +1,498 @@
/* app/shelf_index.c — 书架索引落盘/校验(实现见 app/shelf_index.h)。
*
* 落盘格式 v1(全部小端、手工序列化,不依赖结构体布局):
* [header 24B] "MEOWIDX1" | 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
* [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 校验,杜绝索引坏档造成越界读。
*
* 写入顺序:临时文件 `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','1' };
/* ---- 小端序列化 ---- */
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; /* 换了卡/挂载点 → 重扫 */
/* 目录记录:逐个 stat 比对 mtime(唯一允许的校验 IO,不做 readdir)。 */
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;
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;
}
/* 书记录 */
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;
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;
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);
}