feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)

脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。

- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
  逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
  偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
  rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
  (P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
  各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
  保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
  main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
  env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
  永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
  wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
  IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
  权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步

范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
This commit is contained in:
AskaEth committed 2026-11-04 20:02:28 +08:00
1 parent e87ccdf061
commit 7c7d891570
59 files changed
+5305 -590

No files matched your search

+606 -129
View File
@@ -9,6 +9,7 @@
#include "rdsrc.h"
#include "rdtxt.h"
#include "seg.h"
#include "rdchapter.h"
#include "rdlayout.h"
#include "rdimg.h"
#include "../engine/rdft.h"
@@ -22,6 +23,17 @@ typedef struct seg {
int page_count; /* 本段页数 */
} seg_t;
/* Phase 2C:双段 cps 窗口槽——只保留段解码码点(utf8 转完即 free,不再驻留整段 lines/pages)。 */
typedef struct cps_slot {
int seg; /* 段下标;-1 = 空槽 */
uint32_t *cps;
size_t n;
} cps_slot;
/* 滞回释放/预取阈值(按全书页数百分比;真机可调)。 */
#define RD_WIN_NEAR_PCT 2 /* 距下一段界 ≤2%:预取下一段 */
#define RD_WIN_FAR_PCT 4 /* >4% 滑出:远槽可被下一段预取替换(由距离最近者策略体现) */
struct rd_book {
char path[400];
char font[400];
@@ -40,17 +52,28 @@ struct rd_book {
rd_book_page *pages; int npages; /* flat 页表(文本) */
int cur;
/* 当前渲染段复用缓冲 */
uint8_t *seg_utf8; size_t seg_utf8_cap;
uint32_t *seg_cps; size_t seg_cps_n, seg_cps_cap;
rd_line *seg_lines; rd_page *seg_pages; int seg_nlines, seg_npages;
int seg_cache_idx;
/* Phase 2C:双段 cps 窗口(渲染微排版取码点用;替换旧单段 seg_lines/seg_pages 缓存) */
cps_slot win[2];
/* Phase 2A:拼接页表滚动状态(上一段的「末页页首→段尾」码点 tail;建下一段时拼接) */
uint32_t *tail_cps; size_t tail_n;
int tail_seg; /* tail 所属段;-1 = 无 */
int32_t tail_seg_char; /* tail 在所属段的起始字符偏移 */
/* Phase 2C:渲染微排版复用缓冲(拼接页临时码点 + 一页行缓冲,避免每帧 malloc) */
uint32_t *stitch_cps; size_t stitch_cap;
rd_line *render_lines; int render_lines_cap;
/* R3.2 分段流式(TXT):段目录 + 全局段数 + 已建段数 + 段/页表容量(增量补齐用) */
char seg_dir[400];
int seg_n; /* 段文件总数(manifest);fallback = nsegs(全建) */
int seg_built; /* 已布局段数;fallback = nsegs */
int segs_cap, pages_cap; /* segs[]/pages[] 当前容量(只在 seg 路径增量增长) */
/* R3.3 章节(TXT):解析结果 + 就绪/无章节标志(延迟解析,不拖慢首屏) */
rdchapter chaps;
int chaps_ready; /* 1=chaps 已填充(含确无章节) */
int chaps_none; /* 1=确无章节(扫描过 n==0)或该书不可用 */
};
/* ---- 页索引持久化(MeowData;见 rd_book_set_index_dir) ----
@@ -61,6 +84,7 @@ struct rd_book {
* 纯 libc(无 SDK):reader.c 用 meow_config_dir 推导 MeowData dir 注入;冒烟/无卡时 dir 为空 → 直排不持久。 */
static char g_index_dir[400]; /* 索引缓存目录; 空 = 禁用 */
static int seg_ensure_built(rd_book *b, int upto_seg); /* 段路径补齐(下文定义) */
void rd_book_set_index_dir(const char *dir)
{
if (dir && dir[0]) { strncpy(g_index_dir, dir, sizeof g_index_dir - 1); g_index_dir[sizeof g_index_dir - 1] = 0; }
@@ -80,7 +104,7 @@ void rd_book_set_seg_base(const char *base)
#define IDX_MAGIC1 'I'
#define IDX_MAGIC2 'D'
#define IDX_MAGIC3 'X'
#define IDX_VER 1u
#define IDX_VER 2u
static int fio_rd(FILE *f, void *p, size_t n) { return fread(p, 1, n, f) == n; }
static int fio_wr(FILE *f, const void *p, size_t n) { return fwrite(p, 1, n, f) == n; }
@@ -253,6 +277,267 @@ static void index_cache_save(rd_book *b)
if (ok) { rename(tmp, path); } else { remove(tmp); }
}
/* ---- R3.3 章节缓存(MeowData/os.meow.reader/index/<hash>.chap,键=书字节+编码) ----
* 章节 char_off/seg_idx/seg_char 仅依赖书字节与编码(不含字号/行距/字体/版面),故失效键
* (file_size, file_mtime, enc) 即够,与页索引(含 font/fs/ls/cw/ch)分开、独立秒开。
* 目录为 index 同父;g_index_dir 为空(宿主/无卡)→ 不持久,仅内存。 */
#define CHAP_MAGIC0 'R'
#define CHAP_MAGIC1 'C'
#define CHAP_MAGIC2 'H'
#define CHAP_MAGIC3 'P'
#define CHAP_VER 1u
static int chap_cache_path(const rd_book *b, char *out, int outsz)
{
if (!g_index_dir[0]) return -1;
if (snprintf(out, (size_t)outsz, "%s/%08x.chap", g_index_dir, idx_fnv1a(b->path)) >= outsz) return -1;
return 0;
}
/* 章节缓存键:书字节 + 编码(含 BOM/编码覆盖),跟页索引键不同(无字体/字号/版面)。 */
static int chap_cache_load(rd_book *b)
{
char path[440];
if (chap_cache_path(b, path, sizeof path) != 0) return 0;
FILE *f = fopen(path, "rb");
if (!f) return 0;
uint8_t magic[4];
uint16_t ver = 0;
uint64_t fsize = 0; int64_t fmtime = 0; int32_t fenc = 0;
int32_t nsegs = 0, nch = 0; int64_t total = 0;
int ok = 0;
if (fio_rd(f, magic, sizeof magic) && magic[0] == CHAP_MAGIC0 && magic[1] == CHAP_MAGIC1 &&
magic[2] == CHAP_MAGIC2 && magic[3] == CHAP_MAGIC3 &&
fio_rd(f, &ver, sizeof ver) && ver == CHAP_VER &&
fio_rd(f, &fsize, sizeof fsize) && fio_rd(f, &fmtime, sizeof fmtime) &&
fio_rd(f, &fenc, sizeof fenc) &&
fio_rd(f, &nsegs, sizeof nsegs) && fio_rd(f, &total, sizeof total) &&
fio_rd(f, &nch, sizeof nch) &&
nsegs >= 0 && nch >= 0 && nsegs <= 1000000 && nch <= RDCHAP_MAX_ENTRIES) {
struct stat st;
if (stat(b->path, &st) == 0 && (uint64_t)st.st_size == fsize &&
(int64_t)st.st_mtime == fmtime && (int32_t)b->enc == fenc) {
rdchapter c; rdchapter_init(&c);
c.seg_char_start = (int64_t *)malloc((size_t)(nsegs + 1) * sizeof(int64_t));
c.entries = (rdchapter_entry *)malloc((size_t)(nch ? nch : 1) * sizeof(rdchapter_entry));
if (c.seg_char_start && c.entries &&
fio_rd(f, c.seg_char_start, (size_t)(nsegs + 1) * sizeof(int64_t))) {
int eok = 1;
for (int i = 0; i < nch && eok; i++) {
rdchapter_entry *e = &c.entries[i];
if (!fio_rd(f, &e->char_off, sizeof e->char_off) ||
!fio_rd(f, &e->seg_idx, sizeof e->seg_idx) ||
!fio_rd(f, &e->seg_char, sizeof e->seg_char) ||
!fio_rd(f, &e->number, sizeof e->number) ||
!fio_rd(f, e->title, sizeof e->title)) eok = 0;
}
if (eok) {
c.n = nch; c.nsegs = nsegs;
c.has_pre = (nch > 0 && c.entries[0].char_off == 0 && c.entries[0].number == -1) ? 1 : 0;
snprintf(c.pre_title, sizeof c.pre_title, "%s", (nch > 0) ? c.entries[0].title : "卷首");
b->chaps = c;
b->chaps_ready = 1;
b->chaps_none = (nch == 0);
ok = 1;
} else { rdchapter_free(&c); }
} else { rdchapter_free(&c); }
}
}
fclose(f);
return ok;
}
static void chap_cache_save(rd_book *b)
{
if (!b->chaps_ready || b->chaps.nsegs <= 0) return;
char path[440];
if (chap_cache_path(b, path, sizeof path) != 0) return;
char tmp[480];
if (snprintf(tmp, sizeof tmp, "%s.tmp", path) >= (int)sizeof tmp) return;
FILE *f = fopen(tmp, "wb");
if (!f) return;
struct stat st;
uint64_t fsize = 0; int64_t fmtime = 0;
if (stat(b->path, &st) == 0) { fsize = (uint64_t)st.st_size; fmtime = (int64_t)st.st_mtime; }
const uint8_t magic[4] = { CHAP_MAGIC0, CHAP_MAGIC1, CHAP_MAGIC2, CHAP_MAGIC3 };
uint16_t ver = CHAP_VER;
int32_t nsegs = b->chaps.nsegs, fenc = (int32_t)b->enc, nch = b->chaps.n;
int64_t total = b->chaps.seg_char_start ? b->chaps.seg_char_start[nsegs] : 0;
int ok = 1;
ok = ok && fio_wr(f, magic, sizeof magic) && fio_wr(f, &ver, sizeof ver) &&
fio_wr(f, &fsize, sizeof fsize) && fio_wr(f, &fmtime, sizeof fmtime) &&
fio_wr(f, &fenc, sizeof fenc) &&
fio_wr(f, &nsegs, sizeof nsegs) && fio_wr(f, &total, sizeof total) &&
fio_wr(f, &nch, sizeof nch);
if (ok && nsegs > 0) ok = fio_wr(f, b->chaps.seg_char_start, (size_t)(nsegs + 1) * sizeof(int64_t));
if (ok && nch > 0) {
for (int i = 0; i < nch; i++) {
const rdchapter_entry *e = &b->chaps.entries[i];
ok = ok && fio_wr(f, &e->char_off, sizeof e->char_off) &&
fio_wr(f, &e->seg_idx, sizeof e->seg_idx) &&
fio_wr(f, &e->seg_char, sizeof e->seg_char) &&
fio_wr(f, &e->number, sizeof e->number) &&
fio_wr(f, e->title, sizeof e->title);
}
}
fclose(f);
if (ok) { rename(tmp, path); } else { remove(tmp); }
}
/* 确保章节解析(幂等):命中缓存 → 就绪;未命中 → 扫描(读全部段文件)。
* 可靠性(R3.4):仅「扫描全程成功」才落盘(含无章节 n==0);扫描中途出错(段读/解码/内存)
* 不落盘、本次降级无章节,下次重试。返回:0=成功(就绪);-1=不可用(非 TXT/无段目录);
* -2=扫描出错(不落盘)。 */
static int chapter_ensure(rd_book *b)
{
if (!b) return -1;
if (b->chaps_ready) return 0;
if (b->is_img || b->fmt != RD_FMT_TXT || !b->seg_dir[0]) {
b->chaps_none = 1; b->chaps_ready = 1;
return -1;
}
if (chap_cache_load(b)) return 0;
int rc = rdchapter_scan(b->seg_dir, b->enc, b->base, &b->chaps);
if (rc != 0) { b->chaps_none = 1; b->chaps_ready = 1; return -2; } /* 扫描失败:不落盘,降级无章节 */
b->chaps_none = (b->chaps.n == 0);
b->chaps_ready = 1;
chap_cache_save(b); /* 可靠(成功扫描,无论有无章节)→ 落盘 */
return 0;
}
int rd_book_chapter_ready(rd_book *b) { return (b && b->chaps_ready) ? 1 : 0; }
/* R3.4 手动重解析本书章节:先扫描到临时结果,成功才「清旧缓存/旧内存 + 换入新结果 + 落盘」;
* 失败则保留旧缓存/旧内存(绝不产生错位章节),不落盘、返回 -2。返回:n=章节数 / 0=无章节(成功)/
* -1=不可用(非 TXT+seg)/ -2=扫描失败。幂等。 */
int rd_book_reparse_chapters(rd_book *b)
{
if (!b) return -1;
if (b->is_img || b->fmt != RD_FMT_TXT || !b->seg_dir[0]) return -1;
rdchapter tmp; rdchapter_init(&tmp);
int rc = rdchapter_scan(b->seg_dir, b->enc, b->base, &tmp);
if (rc != 0) { rdchapter_free(&tmp); return -2; } /* 失败:保留旧缓存/旧内存,未落盘 */
rdchapter_free(&b->chaps); /* 清除内存旧解析结果 */
b->chaps = tmp;
b->chaps_none = (b->chaps.n == 0);
b->chaps_ready = 1;
char path[440];
if (chap_cache_path(b, path, sizeof path) == 0) remove(path); /* 清缓存文件 */
chap_cache_save(b); /* 可靠结果 → 落盘 */
return b->chaps.n; /* >=0 */
}
int rd_book_chapter_n(rd_book *b)
{
if (!b) return -1;
if (b->is_img || b->fmt != RD_FMT_TXT) return -1;
if (chapter_ensure(b) != 0) return -1;
return b->chaps.n;
}
int rd_book_chapter(rd_book *b, int i, char *title, int tsz,
int64_t *char_off, int *seg, int32_t *seg_char, int *number)
{
if (!b || !title || i < 0) return -1;
if (chapter_ensure(b) != 0) return -1;
if (i >= b->chaps.n || !b->chaps.entries) return -1;
const rdchapter_entry *e = &b->chaps.entries[i];
if (tsz > 0) { snprintf(title, (size_t)tsz, "%s", e->title); }
if (char_off) *char_off = e->char_off;
if (seg) *seg = e->seg_idx;
if (seg_char) *seg_char = e->seg_char;
if (number) *number = e->number;
return 0;
}
int64_t rd_book_page_char_off(rd_book *b, int page)
{
if (!b || b->npages <= 0 || page < 0 || page >= b->npages) return -1;
if (chapter_ensure(b) != 0) return -1;
if (!b->chaps.seg_char_start || b->chaps.nsegs <= 0) return -1;
int seg = b->pages[page].seg;
if (seg < 0 || seg >= b->chaps.nsegs) return -1;
return b->chaps.seg_char_start[seg] + b->pages[page].seg_char;
}
int rd_book_page_for_char(rd_book *b, int64_t char_off)
{
if (!b || b->npages <= 0) return -1;
if (chapter_ensure(b) != 0) return -1;
int nseg = b->chaps.nsegs;
if (!b->chaps.seg_char_start || nseg <= 0) return -1;
int64_t total = b->chaps.seg_char_start[nseg];
if (total <= 0) return 0;
if (char_off < 0) char_off = 0;
if (char_off >= total) char_off = total - 1;
/* 二分所在段:最大的 i 使 seg_char_start[i] <= char_off */
int lo = 0, hi = nseg - 1, s = 0;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (b->chaps.seg_char_start[mid] <= char_off) { s = mid; lo = mid + 1; }
else hi = mid - 1;
}
if (seg_ensure_built(b, s) != 0) return -1;
if (s >= b->nsegs) s = b->nsegs - 1;
int local = (int)(char_off - b->chaps.seg_char_start[s]);
int p0 = b->segs[s].page_start, pc = b->segs[s].page_count;
if (pc <= 0) return (p0 < b->npages) ? p0 : (b->npages - 1);
/* 段内二分:最后一个 seg_char <= local 的页 = 含该字符的页 */
int lo2 = p0, hi2 = p0 + pc - 1, best = p0;
while (lo2 <= hi2) {
int mid = (lo2 + hi2) / 2;
if (b->pages[mid].seg_char <= local) { best = mid; lo2 = mid + 1; }
else hi2 = mid - 1;
}
return (best < b->npages) ? best : (b->npages - 1);
}
int rd_book_chapter_for_page(rd_book *b, int page, int *idx)
{
if (!b || !idx) return -1;
if (chapter_ensure(b) != 0) return -1;
if (b->chaps.n <= 0) { *idx = -1; return 0; }
if (page < 0 || page >= b->npages) return -1;
int64_t po = rd_book_page_char_off(b, page);
if (po < 0) { *idx = -1; return 0; }
int lo = 0, hi = b->chaps.n - 1, best = -1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (b->chaps.entries[mid].char_off <= po) { best = mid; lo = mid + 1; }
else hi = mid - 1;
}
*idx = best;
return 0;
}
/* ---- 段访问(文件/段模式跳转) ---- */
int rd_book_seg_n(rd_book *b)
{
if (!b || b->is_img || b->fmt != RD_FMT_TXT || !b->seg_dir[0]) return -1;
return (b->seg_n > 0) ? b->seg_n : b->nsegs;
}
int rd_book_seg_page(rd_book *b, int seg)
{
if (!b || seg < 0) return -1;
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT) {
if (seg >= b->seg_n) return -1;
if (seg_ensure_built(b, seg) != 0) return -1;
if (seg >= b->nsegs) return -1;
return b->segs[seg].page_start;
}
return (seg < b->nsegs) ? b->segs[seg].page_start : -1;
}
int rd_book_seg_page_count(rd_book *b, int seg)
{
if (!b || seg < 0 || seg >= b->nsegs) return -1;
return b->segs[seg].page_count;
}
int rd_book_seg_of_page(rd_book *b, int page)
{
if (!b || b->npages <= 0 || page < 0 || page >= b->npages) return -1;
return b->pages[page].seg;
}
/* ---- 文本:建索引(逐段 layout) ---- */
static int build_index(rd_book *b)
{
@@ -288,8 +573,10 @@ static int build_index(rd_book *b)
b->segs[seg_idx].page_count = out.npages;
for (int p = 0; p < out.npages; p++) {
if (b->npages >= (int)cap_pages) { cap_pages *= 2; b->pages = (rd_book_page *)realloc(b->pages, cap_pages * sizeof(rd_book_page)); }
memset(&b->pages[b->npages], 0, sizeof b->pages[b->npages]);
b->pages[b->npages].seg = seg_idx;
b->pages[b->npages].seg_char = out.pages[p].first_char;
b->pages[b->npages].flags = (uint8_t)(out.lines[out.pages[p].first_line].para_start ? 1u : 0u);
b->npages++;
}
free(lines); free(pages);
@@ -300,41 +587,169 @@ static int build_index(rd_book *b)
return 0;
}
/* ---- R3.2 分段流式(TXT):把一段 UTF-8 布局并追加到 flat 页表 ---- */
static int append_seg_pages(rd_book *b, int seg_idx, const uint8_t *utf8, size_t ulen)
/* ---- 段解码:把段 idx 的原始字节 → utf8 → 码点(malloc 返回,调用方 free)。
* seg 路径读 512KB 段文件;非 seg 路径按 rd_src_seg 重显。窗口/建表/tail 重建共用。 ---- */
static int seg_load_cps(rd_book *b, int seg_idx, uint32_t **cps_out, size_t *n_out)
{
uint32_t *cps = NULL; size_t n = 0;
if (rd_utf8_to_cp(utf8, ulen, &cps, &n) != RD_ENC_OK) return -5;
rd_line *lines = (rd_line *)malloc((n ? n : 1) * sizeof(rd_line));
rd_page *pages = (rd_page *)malloc((n ? n : 1) * sizeof(rd_page));
if (!lines || !pages) { free(lines); free(pages); free(cps); return -6; }
rd_layout_in in;
rd_layout_out out;
in.chars = cps; in.n = (int32_t)n; in.fs = b->fs; in.line_scale = b->ls;
in.page_w = b->content_w; in.page_h = b->content_h; in.indent = b->fs;
in.meas = book_meas; in.mctx = b;
out.lines = lines; out.pages = pages; out.max_lines = (int)(n ? n : 1); out.max_pages = (int)(n ? n : 1);
int lrc = rd_layout(&in, &out);
free(cps);
if (lrc != RD_LAYOUT_OK) { free(lines); free(pages); return -4; }
b->segs[seg_idx].page_start = b->npages;
b->segs[seg_idx].page_count = out.npages;
for (int p = 0; p < out.npages; p++) {
if (b->npages >= b->pages_cap) {
int nc = b->pages_cap ? b->pages_cap * 2 : 1024;
rd_book_page *np = (rd_book_page *)realloc(b->pages, (size_t)nc * sizeof(rd_book_page));
if (!np) { free(lines); free(pages); return -6; }
b->pages = np; b->pages_cap = nc;
}
b->pages[b->npages].seg = seg_idx;
b->pages[b->npages].seg_char = out.pages[p].first_char;
b->npages++;
if (!cps_out || !n_out) return -1;
*cps_out = NULL; *n_out = 0;
uint8_t *utf8 = NULL; size_t cap = 0, ulen = 0;
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT) {
size_t rawlen = 0;
if (rdseg_rawlen(b->seg_dir, seg_idx, &rawlen) != 0) return -4;
cap = rawlen * 3 / 2 + 16;
if (cap < RDTXT_SEG_UTF8_MAX + 8) cap = RDTXT_SEG_UTF8_MAX + 8;
utf8 = (uint8_t *)malloc(cap);
if (!utf8) return -6;
uint8_t *raw = (uint8_t *)malloc(rawlen ? rawlen : 1);
if (!raw) { free(utf8); return -6; }
size_t rn = 0;
int rc = rdseg_load(b->seg_dir, seg_idx, raw, rawlen, &rn);
if (rc != 0) { free(raw); free(utf8); return -4; }
int r2 = rd_to_utf8(b->enc, raw, rn, utf8, cap, &ulen);
free(raw);
if (r2 == RD_ENC_ERR_BUFSZ) { free(utf8); return -2; }
if (r2 != RD_ENC_OK) { free(utf8); return -3; }
} else {
cap = RDTXT_SEG_UTF8_MAX + 8;
utf8 = (uint8_t *)malloc(cap);
if (!utf8) return -6;
if (rd_src_seg(b->src, seg_idx, utf8, cap, &ulen) != 0) { free(utf8); return -4; }
}
free(lines); free(pages);
uint32_t *cps = NULL; size_t n = 0;
int rc = rd_utf8_to_cp(utf8, ulen, &cps, &n);
free(utf8);
if (rc != RD_ENC_OK) { free(cps); return -5; }
*cps_out = cps; *n_out = n;
return 0;
}
/* ---- R3.2 段路径:把 [seg_built, upto_seg] 逐段(512KB 段文件 → utf8 → 布局)补齐进 flat 页表。
/* ---- Phase 2A:拼接页表滚动状态(tail) ---- */
static void tail_clear(rd_book *b)
{
free(b->tail_cps);
b->tail_cps = NULL;
b->tail_n = 0;
b->tail_seg = -1;
b->tail_seg_char = 0;
}
/* 缓存续建时 tail 不在内存(未持久化)→ 由段 seg 的末页首字符重算(读该段 cps 截取末页 tail)。 */
static int tail_rebuild(rd_book *b, int seg)
{
if (b->tail_seg == seg && b->tail_cps && b->tail_n > 0) return 0;
tail_clear(b);
if (seg < 0 || seg >= b->seg_built) return -1;
int pc = b->segs[seg].page_count;
if (pc <= 0) return -1;
int lastp = b->segs[seg].page_start + pc - 1;
if (lastp < 0 || lastp >= b->npages) return -1;
int32_t sc = b->pages[lastp].seg_char;
if (sc < 0) return -1;
uint32_t *cps = NULL; size_t n = 0;
if (seg_load_cps(b, seg, &cps, &n) != 0) return -1;
if ((size_t)sc > n) { free(cps); return -1; }
b->tail_n = n - (size_t)sc;
b->tail_cps = (uint32_t *)malloc((b->tail_n ? b->tail_n : 1) * sizeof(uint32_t));
if (!b->tail_cps) { free(cps); return -6; }
if (b->tail_n) memcpy(b->tail_cps, cps + sc, b->tail_n * sizeof(uint32_t));
b->tail_seg = seg;
b->tail_seg_char = sc;
free(cps);
return 0;
}
static int pages_push(rd_book *b, int seg, int32_t seg_char, uint8_t flags)
{
if (b->npages >= b->pages_cap) {
int nc = b->pages_cap ? b->pages_cap * 2 : 1024;
rd_book_page *np = (rd_book_page *)realloc(b->pages, (size_t)nc * sizeof(rd_book_page));
if (!np) return -6;
b->pages = np; b->pages_cap = nc;
}
memset(&b->pages[b->npages], 0, sizeof b->pages[b->npages]);
b->pages[b->npages].seg = seg;
b->pages[b->npages].seg_char = seg_char;
b->pages[b->npages].flags = flags;
b->npages++;
return 0;
}
/* ---- Phase 2A:滚动拼接。把 seg_idx 段码点布局并追加到 flat 页表。
* 建 N+1 时:删 N 的临时末页 → tail_cps + cps_new 拼接重排 → 首张拼接页回填 N(锚不变),
* 其余页按 first_char − tail_n 归入 N+1。末段/首段无 tail 时按整段直排。 ---- */
static int append_seg_pages(rd_book *b, int seg_idx, const uint32_t *cps, size_t n)
{
int have_tail = (seg_idx > 0 && b->tail_seg == seg_idx - 1 && b->tail_cps && b->tail_n > 0);
if (have_tail) {
if (b->segs[seg_idx - 1].page_count <= 0) return -4;
b->npages--; /* 删上一段临时末页(拼接页原位回填) */
b->segs[seg_idx - 1].page_count--;
}
size_t stitched_n = have_tail ? b->tail_n + n : n;
uint32_t *scps = NULL;
const uint32_t *chars;
if (have_tail) {
scps = (uint32_t *)malloc((stitched_n ? stitched_n : 1) * sizeof(uint32_t));
if (!scps) return -6;
memcpy(scps, b->tail_cps, b->tail_n * sizeof(uint32_t));
memcpy(scps + b->tail_n, cps, n * sizeof(uint32_t));
chars = scps;
} else {
chars = cps;
}
rd_line *lines = (rd_line *)malloc((stitched_n ? stitched_n : 1) * sizeof(rd_line));
rd_page *pages = (rd_page *)malloc((stitched_n ? stitched_n : 1) * sizeof(rd_page));
if (!lines || !pages) { free(lines); free(pages); free(scps); return -6; }
rd_layout_in in;
rd_layout_out out;
in.chars = chars; in.n = (int32_t)stitched_n; in.fs = b->fs; in.line_scale = b->ls;
in.page_w = b->content_w; in.page_h = b->content_h; in.indent = b->fs;
in.meas = book_meas; in.mctx = b;
out.lines = lines; out.pages = pages; out.max_lines = (int)(stitched_n ? stitched_n : 1); out.max_pages = (int)(stitched_n ? stitched_n : 1);
int lrc = rd_layout(&in, &out);
if (lrc != RD_LAYOUT_OK) { free(lines); free(pages); free(scps); return -4; }
int first_own = 0;
if (have_tail) {
if (out.npages < 1) { free(lines); free(pages); free(scps); return -4; }
int rc = pages_push(b, seg_idx - 1, b->tail_seg_char,
(uint8_t)(out.lines[out.pages[0].first_line].para_start ? 1u : 0u));
if (rc != 0) { free(lines); free(pages); free(scps); return rc; }
b->segs[seg_idx - 1].page_count++;
first_own = 1;
}
b->segs[seg_idx].page_start = b->npages;
b->segs[seg_idx].page_count = 0;
for (int p = first_own; p < out.npages; p++) {
int32_t local = out.pages[p].first_char - (have_tail ? (int32_t)b->tail_n : 0);
if (local < 0) local = 0;
int rc = pages_push(b, seg_idx, local,
(uint8_t)(out.lines[out.pages[p].first_line].para_start ? 1u : 0u));
if (rc != 0) { free(lines); free(pages); free(scps); return rc; }
b->segs[seg_idx].page_count++;
}
/* 保存本段末页 tail(供建下一段拼接;末段也保存但不会用到)。 */
tail_clear(b);
if (b->segs[seg_idx].page_count > 0) {
int lastp = b->segs[seg_idx].page_start + b->segs[seg_idx].page_count - 1;
int32_t sc = b->pages[lastp].seg_char;
if (sc < 0 || (size_t)sc > n) { free(lines); free(pages); free(scps); return -4; }
b->tail_n = n - (size_t)sc;
b->tail_cps = (uint32_t *)malloc((b->tail_n ? b->tail_n : 1) * sizeof(uint32_t));
if (!b->tail_cps) { b->tail_n = 0; free(lines); free(pages); free(scps); return -6; }
if (b->tail_n) memcpy(b->tail_cps, cps + sc, b->tail_n * sizeof(uint32_t));
b->tail_seg = seg_idx;
b->tail_seg_char = sc;
}
free(lines); free(pages); free(scps);
return 0;
}
/* ---- R3.2 段路径:把 [seg_built, upto_seg] 逐段(512KB 段文件 → cps → 拼接布局)补齐进 flat 页表。
* 幂等:已建段跳过。开书只建到目标(页 0/保存进度),翻页/跳转再按需补齐 —— 免整书一次性布局。 */
static int seg_ensure_built(rd_book *b, int upto_seg)
{
@@ -345,8 +760,6 @@ static int seg_ensure_built(rd_book *b, int upto_seg)
int built_any = 0;
while (b->seg_built <= upto_seg) {
int idx = b->seg_built;
size_t rawlen = 0;
if (rdseg_rawlen(b->seg_dir, idx, &rawlen) != 0) break;
if (b->segs_cap <= idx) {
int nc = b->segs_cap ? b->segs_cap * 2 : 64;
if (nc <= idx) nc = idx + 1;
@@ -354,24 +767,13 @@ static int seg_ensure_built(rd_book *b, int upto_seg)
if (!ns) return -6;
b->segs = ns; b->segs_cap = nc;
}
size_t cap = rawlen * 3 / 2 + 16;
if (cap < RDTXT_SEG_UTF8_MAX + 8) cap = RDTXT_SEG_UTF8_MAX + 8;
if (!b->seg_utf8 || b->seg_utf8_cap < cap) {
uint8_t *t = (uint8_t *)malloc(cap);
if (!t) return -6;
free(b->seg_utf8); b->seg_utf8 = t; b->seg_utf8_cap = cap;
}
uint8_t *raw = (uint8_t *)malloc(rawlen ? rawlen : 1);
if (!raw) return -6;
size_t rn = 0;
if (rdseg_load(b->seg_dir, idx, raw, rawlen, &rn) != 0) { free(raw); return -4; }
size_t ulen = 0;
int rc = rd_to_utf8(b->enc, raw, rn, b->seg_utf8, cap, &ulen);
free(raw);
if (rc == RD_ENC_ERR_BUFSZ) return -2;
if (rc != RD_ENC_OK) return -3;
int prc = append_seg_pages(b, idx, b->seg_utf8, ulen);
if (prc != 0) return prc;
if (idx > 0 && tail_rebuild(b, idx - 1) != 0) return -4;
uint32_t *cps = NULL; size_t n = 0;
int rc = seg_load_cps(b, idx, &cps, &n);
if (rc != 0) return rc;
rc = append_seg_pages(b, idx, cps, n);
free(cps);
if (rc != 0) return rc;
b->seg_built++;
built_any = 1;
}
@@ -380,59 +782,53 @@ static int seg_ensure_built(rd_book *b, int upto_seg)
return 0;
}
/* ---- 正文重显 + 布局到复用缓冲 ---- */
static int load_seg(rd_book *b, int seg_idx)
/* ---- Phase 2C:双段 cps 窗口(渲染微排版取码点;替换旧单段整段 lines/pages 缓存) ---- */
static void win_clear(rd_book *b)
{
if (seg_idx == b->seg_cache_idx) return 0;
size_t ulen = 0;
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT) {
/* R3.2 段路径:只读当前页所在 512KB 段文件(有界,不全量读整书) */
size_t rawlen = 0;
if (rdseg_rawlen(b->seg_dir, seg_idx, &rawlen) != 0) return -4;
size_t cap = rawlen * 3 / 2 + 16;
if (cap < RDTXT_SEG_UTF8_MAX + 8) cap = RDTXT_SEG_UTF8_MAX + 8;
if (!b->seg_utf8 || b->seg_utf8_cap < cap) {
uint8_t *t = (uint8_t *)malloc(cap);
if (!t) return -6;
free(b->seg_utf8); b->seg_utf8 = t; b->seg_utf8_cap = cap;
}
uint8_t *raw = (uint8_t *)malloc(rawlen ? rawlen : 1);
if (!raw) return -6;
size_t rn = 0;
int rc = rdseg_load(b->seg_dir, seg_idx, raw, rawlen, &rn);
if (rc != 0) { free(raw); return -4; }
int r2 = rd_to_utf8(b->enc, raw, rn, b->seg_utf8, cap, &ulen);
free(raw);
if (r2 == RD_ENC_ERR_BUFSZ) return -2;
if (r2 != RD_ENC_OK) return -3;
} else {
size_t cap = RDTXT_SEG_UTF8_MAX + 8;
if (!b->seg_utf8) b->seg_utf8 = (uint8_t *)malloc(cap);
if (!b->seg_utf8) return -6;
if (rd_src_seg(b->src, seg_idx, b->seg_utf8, cap, &ulen) != 0) return -4;
for (int i = 0; i < 2; i++) {
free(b->win[i].cps);
b->win[i].cps = NULL;
b->win[i].n = 0;
b->win[i].seg = -1;
}
size_t n = 0;
free(b->seg_cps); b->seg_cps = NULL;
if (rd_utf8_to_cp(b->seg_utf8, ulen, &b->seg_cps, &n) != RD_ENC_OK) return -5;
b->seg_cps_n = n;
if (b->seg_lines) free(b->seg_lines);
if (b->seg_pages) free(b->seg_pages);
b->seg_lines = (rd_line *)malloc((n ? n : 1) * sizeof(rd_line));
b->seg_pages = (rd_page *)malloc((n ? n : 1) * sizeof(rd_page));
if (!b->seg_lines || !b->seg_pages) return -6;
rd_layout_in in;
rd_layout_out out;
in.chars = b->seg_cps; in.n = (int32_t)n; in.fs = b->fs; in.line_scale = b->ls;
in.page_w = b->content_w; in.page_h = b->content_h; in.indent = b->fs;
in.meas = book_meas; in.mctx = b;
out.lines = b->seg_lines; out.pages = b->seg_pages; out.max_lines = (int)(n ? n : 1); out.max_pages = (int)(n ? n : 1);
int lrc = rd_layout(&in, &out);
if (lrc != RD_LAYOUT_OK) { b->seg_nlines = b->seg_npages = 0; b->seg_cache_idx = -1; return -4; }
b->seg_nlines = out.nlines; b->seg_npages = out.npages;
b->seg_cache_idx = seg_idx;
}
static cps_slot *win_find(rd_book *b, int seg)
{
if (b->win[0].seg == seg) return &b->win[0];
if (b->win[1].seg == seg) return &b->win[1];
return NULL;
}
static int win_load_seg(rd_book *b, int seg_idx, cps_slot *slot)
{
uint32_t *cps = NULL; size_t n = 0;
int rc = seg_load_cps(b, seg_idx, &cps, &n);
if (rc != 0) return rc;
free(slot->cps);
slot->cps = cps; slot->n = n; slot->seg = seg_idx;
return 0;
}
/* 确保段 seg_idx 码点在窗口内。双槽都占时,淘汰「离 seg_idx 更远」的槽(距相等淘汰更小段下标,
* 倾向保留下一段);滞回释放由 reader_on_tick 的 2%/4% 预取门控体现。 */
static int win_ensure(rd_book *b, int seg_idx)
{
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
if (win_find(b, seg_idx)) return 0;
int victim = -1;
if (b->win[0].seg < 0) victim = 0;
else if (b->win[1].seg < 0) victim = 1;
else {
int d0 = b->win[0].seg - seg_idx; if (d0 < 0) d0 = -d0;
int d1 = b->win[1].seg - seg_idx; if (d1 < 0) d1 = -d1;
if (d0 > d1) victim = 0;
else if (d1 > d0) victim = 1;
else victim = (b->win[0].seg < b->win[1].seg) ? 0 : 1;
}
return win_load_seg(b, seg_idx, &b->win[victim]);
}
int rd_book_open(rd_book **b, const char *path, const char *font_path,
int fs, int ls, int pw, int ph, int mx, int my, int top)
{
@@ -465,7 +861,8 @@ int rd_book_open(rd_book **b, const char *path, const char *font_path,
if (rdft_init(&x->ft) != 0) goto close;
if (font_path && rdft_load_font(x->ft, x->font) != 0) { rdft_free(x->ft); x->ft = NULL; goto close; }
if (x->ft) rdft_set_px(x->ft, fs);
x->seg_cache_idx = -1;
x->win[0].seg = x->win[1].seg = -1;
x->tail_seg = -1;
int seg_mode = 0;
/* R3.2 分段流式(TXT 且段 base 已配置):开书切 512KB 段文件,逐段按需布局(不全量)。 */
if (g_seg_base[0] && x->fmt == RD_FMT_TXT) {
@@ -513,10 +910,11 @@ void rd_book_close(rd_book *b)
if (!b) return;
free(b->segs);
free(b->pages);
free(b->seg_utf8);
free(b->seg_cps);
free(b->seg_lines);
free(b->seg_pages);
win_clear(b);
tail_clear(b);
free(b->stitch_cps);
free(b->render_lines);
rdchapter_free(&b->chaps);
if (b->ft) rdft_free(b->ft);
if (b->src) rd_src_free(b->src);
free(b);
@@ -563,6 +961,8 @@ int rd_book_set_enc(rd_book *b, rd_enc_t enc)
/* 重建索引(复用现值 fs/ls/content_*),再回锚 */
free(b->segs); free(b->pages); b->segs = NULL; b->pages = NULL;
b->nsegs = b->npages = 0; b->seg_built = 0; b->segs_cap = b->pages_cap = 0;
win_clear(b); /* 编码变了 → 码点空间不同,窗口/tail 全失效 */
tail_clear(b);
if (b->seg_dir[0]) {
/* R3.2 段路径:编码变了 → 段文件边界/解码不同 → 重建段文件 + 按需布局(段 0 起步) */
if (oldenc != b->enc) rdseg_clear(b->seg_dir);
@@ -615,27 +1015,104 @@ int rd_book_set_page(rd_book *b, int page)
return page;
}
/* Phase 2C:空闲预取下一段(reader_on_tick 调)。当前页距下一段界 ≤2% 时把 s+1 段码点
* 提前解码进窗口(命中/未到阈值则幂等返回),翻到段界即零突发。 */
int rd_book_prefetch(rd_book *b)
{
if (!b || b->is_img || b->npages <= 0) return -1;
if (!(b->seg_dir[0] && b->fmt == RD_FMT_TXT)) return 0; /* 仅 TXT 段路径有段界预取意义 */
int cur = b->cur; if (cur < 0) cur = 0; if (cur >= b->npages) cur = b->npages - 1;
int s = b->pages[cur].seg;
if (s < 0 || s + 1 >= b->nsegs) return 0;
int bnd = b->segs[s + 1].page_start;
int d = cur - bnd; if (d < 0) d = -d;
if ((long long)d * 100 / b->npages > RD_WIN_NEAR_PCT) return 0;
return win_ensure(b, s + 1);
}
/* ---- Phase 2C:页级微排版。把 page_no 从页首字符起只排一页(复用 rd_layout_page)。
* 拼接页(TXT 段界末页)在窗口两段 cps 相接的临时缓冲上排,天然连续跨段。
* 输出:*chars 与 *chars_n = 绘制用码点视图(窗口 cps 或 b->stitch_cps,下次调用前勿 free),
* b->render_lines 填行;*nlines 行数。返回 0=成功,1=空页,负=错误。 */
static int page_micro_layout(rd_book *b, int page_no, const uint32_t **chars, size_t *chars_n, int *nlines)
{
*chars = NULL; *chars_n = 0; *nlines = 0;
rd_book_page *pg = &b->pages[page_no];
int seg_idx = (int)pg->seg;
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
int stitched = (b->seg_dir[0] && b->fmt == RD_FMT_TXT) &&
(page_no == b->segs[seg_idx].page_start + b->segs[seg_idx].page_count - 1) &&
(seg_idx + 1 < b->nsegs);
if (win_ensure(b, seg_idx) != 0) return -2;
if (stitched && win_ensure(b, seg_idx + 1) != 0) return -2;
int lh = (b->fs * b->ls) / 100; if (lh < 1) lh = 1;
int max_lines = b->content_h / lh + 2;
if (b->render_lines_cap < max_lines) {
rd_line *t = (rd_line *)realloc(b->render_lines, (size_t)max_lines * sizeof(rd_line));
if (!t) return -6;
b->render_lines = t; b->render_lines_cap = max_lines;
}
cps_slot *s0 = win_find(b, seg_idx);
const uint32_t *view; size_t view_n; int32_t start;
if (!stitched) {
view = s0->cps; view_n = s0->n;
start = pg->seg_char;
if (start < 0) start = 0;
if ((size_t)start > view_n) start = (int32_t)view_n;
} else {
cps_slot *s1 = win_find(b, seg_idx + 1);
int32_t tail = pg->seg_char; if (tail < 0) tail = 0;
if ((size_t)tail > s0->n) return 1;
size_t tail_n = s0->n - (size_t)tail;
/* 拼接页只吃「一页能装下」的下一段前缀;按一页行数×行宽×2(零宽字符余量)估上界。 */
size_t budget = (size_t)(max_lines + 2) * (size_t)(b->content_w + 2) * 2u + 64u;
size_t take = s1->n < budget ? s1->n : budget;
size_t total = tail_n + take;
if (b->stitch_cap < total) {
uint32_t *t = (uint32_t *)realloc(b->stitch_cps, (total ? total : 1) * sizeof(uint32_t));
if (!t) return -6;
b->stitch_cps = t; b->stitch_cap = total;
}
memcpy(b->stitch_cps, s0->cps + tail, tail_n * sizeof(uint32_t));
memcpy(b->stitch_cps + tail_n, s1->cps, take * sizeof(uint32_t));
view = b->stitch_cps; view_n = total;
start = 0;
}
int mid_para = !(pg->flags & 1u);
rd_layout_in in;
in.chars = view; in.n = (int32_t)view_n; in.fs = b->fs; in.line_scale = b->ls;
in.page_w = b->content_w; in.page_h = b->content_h; in.indent = b->fs;
in.meas = book_meas; in.mctx = b;
int32_t next_start = 0;
int rc = rd_layout_page(&in, start, mid_para, b->render_lines, max_lines, nlines, &next_start);
if (rc == RD_LAYOUT_ERR_OVER) return -4;
if (rc != RD_LAYOUT_OK) return -3;
*chars = view; *chars_n = view_n;
return (*nlines > 0) ? 0 : 1;
}
/* ---- 文本页渲染(FreeType 字形 → 全屏 565) ---- */
static int render_text_page(rd_book *b, int page_no, uint16_t *out, int pitch, uint16_t bg565, uint16_t fg565)
{
int npix = b->pw * b->ph;
for (int i = 0; i < npix; i++) out[i] = bg565;
rd_book_page *pg = &b->pages[page_no];
int seg_idx = (int)pg->seg;
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
if (load_seg(b, seg_idx) != 0) return -2;
int local_page = page_no - b->segs[seg_idx].page_start;
if (local_page < 0 || local_page >= b->seg_npages) return 1;
rd_page *pp = &b->seg_pages[local_page];
const uint32_t *chars = NULL; size_t chars_n = 0; int nlines = 0;
int rc = page_micro_layout(b, page_no, &chars, &chars_n, &nlines);
if (rc != 0) return rc;
int asc = rdft_ascender(b->ft);
int screen_x0 = b->mx;
int screen_y0 = b->top;
for (int li = 0; li < pp->nlines; li++) {
rd_line *ln = &b->seg_lines[pp->first_line + li];
for (int li = 0; li < nlines; li++) {
rd_line *ln = &b->render_lines[li];
int pen = screen_x0 + ln->x;
int baseline = screen_y0 + ln->y + asc;
for (int32_t ci = ln->s; ci < ln->e; ci++) {
uint32_t cp = b->seg_cps[ci];
if (ci < 0 || (size_t)ci >= chars_n) continue;
uint32_t cp = chars[ci];
int adv = 0, bw = 0, bh = 0, left = 0, btop = 0;
const uint8_t *bmp = NULL;
rdft_glyph(b->ft, cp, &adv, &bmp, &bw, &bh, &left, &btop);
@@ -704,6 +1181,8 @@ int rd_book_jump_to_anchor(rd_book *b, int seg, int32_t ch, int fs, int ls)
b->content_w = b->pw - 2 * b->mx;
b->content_h = b->ph - b->top - b->my;
if (b->ft) rdft_set_px(b->ft, fs);
win_clear(b); /* 页表重建 → 窗口/tail 状态整体复位(码点可复用,但按重建统一清) */
tail_clear(b);
seg_t *nos = b->segs; int n_oldsegs = b->nsegs;
rd_book_page *nop = b->pages; int n_oldpages = b->npages;
b->segs = NULL; b->pages = NULL; b->nsegs = b->npages = 0;
@@ -740,22 +1219,20 @@ int rd_book_jump_to_anchor(rd_book *b, int seg, int32_t ch, int fs, int ls)
int rd_book_render_text(rd_book *b, int page_no, rd_book_line_fn fn, void *ctx)
{
if (!b || !fn || b->is_img || b->npages <= 0 || page_no < 0 || page_no >= b->npages) return -1;
rd_book_page *pg = &b->pages[page_no];
int seg_idx = (int)pg->seg;
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
if (load_seg(b, seg_idx) != 0) return -2;
int local_page = page_no - b->segs[seg_idx].page_start;
if (local_page < 0 || local_page >= b->seg_npages) return 1;
rd_page *pp = &b->seg_pages[local_page];
const uint32_t *chars = NULL; size_t chars_n = 0; int nlines = 0;
int rc = page_micro_layout(b, page_no, &chars, &chars_n, &nlines);
if (rc != 0) return rc;
uint8_t *line_utf8 = (uint8_t *)malloc((size_t)(b->content_w * 4 + 32));
if (!line_utf8) return -6;
for (int li = 0; li < pp->nlines; li++) {
rd_line *ln = &b->seg_lines[pp->first_line + li];
for (int li = 0; li < nlines; li++) {
rd_line *ln = &b->render_lines[li];
int x = b->mx + ln->x, y = b->top + ln->y;
size_t o = 0;
for (int32_t ci = ln->s; ci < ln->e; ci++) {
if (ci < 0 || (size_t)ci >= chars_n) continue;
uint8_t tmp[4];
int n = rd_cp_to_utf8(b->seg_cps[ci], tmp);
int n = rd_cp_to_utf8(chars[ci], tmp);
if (o + (size_t)n + 1 > (size_t)(b->content_w * 4 + 32)) break;
memcpy(line_utf8 + o, tmp, (size_t)n); o += (size_t)n;
}
line_utf8[o] = 0;
+21
View File
@@ -24,6 +24,7 @@ typedef struct rd_book rd_book;
typedef struct rd_book_page {
int32_t seg; /* 段下标 */
int32_t seg_char; /* 段内字符偏移(页首) */
uint8_t flags; /* bit0 = 首页行是否段首行(缩进/段距判定;Phase 2A/2C) */
} rd_book_page;
/* 打开书:path=TXT;font_path=FreeType 字体;geom=版面(fs 字号 / ls 行距% / pw,ph 全屏 /
@@ -36,6 +37,7 @@ int rd_book_pages(rd_book *b);
int rd_book_current(rd_book *b); /* 当前页 */
int rd_book_turn(rd_book *b, int delta); /* +1/-1;clamp 到 [0,npages-1] */
int rd_book_set_page(rd_book *b, int page); /* 跳到绝对页;clamp;返回实际页(段刮擦用) */
int rd_book_prefetch(rd_book *b); /* 空闲预取下一段(reader_on_tick 调);0=已备/无需 */
int rd_book_is_image(rd_book *b); /* 1=CBZ 图片页书 */
/* 目录:0=无;i 项标题写到 title,目标段/字符偏移填 *seg 与 *ch。 */
@@ -73,6 +75,25 @@ void rd_book_set_index_dir(const char *dir);
* 码位对齐段文件,渲染只加载当前页所在段(不全量读整书)——治大书开书 CPU100%/返回无效。 */
void rd_book_set_seg_base(const char *base);
/* R3.3(TXT 章节):章节规范解析(core/rdchapter.c)+ 章节→页映射。
* 只对 TXT(seg_mode 分段流式路径)生效;其它格式返回「无章节」,不破坏既有路径。
* rd_book_chapter_n 触发解析(首次读全部段文件解码扫行,或命中章节缓存秒回),
* 解析/缓存后就绪;顶栏章节名与「当前页属于哪章」据此工作。 */
int rd_book_chapter_n(rd_book *b); /* 章节数;-1=非 TXT/CBZ(不可用) */
int rd_book_chapter_ready(rd_book *b); /* 1=章节已解析(含无章节=0 但已确认) */
int rd_book_reparse_chapters(rd_book *b); /* 手动重解析:清除缓存+重扫+落盘;见 book.c */
int rd_book_chapter(rd_book *b, int i, char *title, int tsz,
int64_t *char_off, int *seg, int32_t *seg_char, int *number);
int rd_book_chapter_for_page(rd_book *b, int page, int *idx); /* 当前页所属章节下标;-1=无 */
int rd_book_page_for_char(rd_book *b, int64_t char_off); /* 全书字符偏移 → 页号;-1=不可用 */
int64_t rd_book_page_char_off(rd_book *b, int page); /* 页首全书字符偏移;-1=不可用 */
/* 段(文件/段模式跳转用)。非段模式返回 -1。 */
int rd_book_seg_n(rd_book *b); /* 段数 */
int rd_book_seg_page(rd_book *b, int seg); /* 段首页(ensure built);-1 */
int rd_book_seg_page_count(rd_book *b, int seg); /* 段页数;-1 */
int rd_book_seg_of_page(rd_book *b, int page); /* 页所属段;-1 */
/* 字体/几何 */
int rd_book_font(rd_book *b); /* 当前字号 */
int rd_book_ls(rd_book *b); /* 行距% */
+478 -42
View File
@@ -10,6 +10,7 @@
#include "book.h"
#include "progress.h"
#include "reader_conf.h"
#include "rdchapter.h" /* RDCHAP_TITLE_MAX(章节标题) */
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
@@ -18,7 +19,7 @@
#include <time.h>
#define RD_MENU_H 64
#define RD_MENU_BTN 4 /* 返回 | 目录 | 界面 | 设置(MVP 单条) */
#define RD_MENU_BTN 5 /* 返回 | 目录 | 跳转 | 界面 | 设置(R3.3 加「跳转」) */
#define RD_HEADER_H 30 /* 页头条高(书名+章节名) */
#define RD_FOOT_H 18 /* 页底条高(章节+页码) */
#define RD_UI_H 268 /* 「界面」排版面板高 */
@@ -36,6 +37,13 @@ static int g_menu_open;
static int g_ui_open; /* 「界面」排版面板(B) */
static int g_dir_open; /* 目录覆盖层 */
static int g_dir_top, g_dir_row_h; /* 目录列表几何(rdsrc 目录项) */
static int g_jump_open; /* 跳转覆盖层(R3.3) */
static int g_jump_mode; /* 1=章节模式; 2=文件/段模式; 0=未定 */
static int g_jump_sel; /* 当前选中章节/段下标 */
static int g_jump_scroll; /* 列表滚动(首行项下标) */
static char g_jump_buf[8]; /* 章内/段内页码输入缓冲(ASCII 数字) */
static int g_chap_cache_idx = -1; /* 章节范围缓存(章起/止页;随字号/行距重排失效) */
static int g_chap_cache_start, g_chap_cache_end;
static char g_font_path[400];
static int g_notice_ms;
@@ -48,12 +56,13 @@ static int g_anim_dir; /* 1=下一页(旧左移/新右入);-
#define RD_SCRUB_W 26 /* 右缘刮擦条宽度 */
static int g_scrub; /* 1=刮擦中 */
static int g_scrub_page; /* 刮擦目标页(实时预览用) */
static int g_scrub_moved; /* 本次刮擦是否发生了拖动(无拖动=点按,不当翻页跳页) */
/* ---- R3 编码切换(TXT;Big5 繁体书手动选定) ---- */
static int g_enc_idx; /* g_enc_cycle[] 下标(当前有效编码) */
static uint32_t g_page_bg, g_page_fg; /* 正文页底/字色(24bit) */
static char g_chapter[128]; /* 顶栏章节名(TXT 无章节=文件名) */
static char g_chapter[RDCHAP_TITLE_MAX]; /* 顶栏章节名(TXT 无章节=文件名) */
static int g_show_header, g_show_footer; /* 页头条/底条显隐(rdconf.rd.header/footer) */
static volatile int g_volpage_pending; /* 系统翻页信号待消费(IPC 线程置位,UI tick 消费,E) */
static int g_prog_dirty; /* 阅读进度变更待写(翻页/刮擦/字号/编码;R3.2 定时落盘防崩溃丢进度) */
@@ -168,7 +177,21 @@ static const char *resolve_font(void)
static void chapter_name(void)
{
/* TXT 无章节 → 文件名;R1 固定。 */
/* TXT 有章节(已解析)→ 当前页所属章名;否则文件名(R1 固定)。
* 仅在章节已就绪时用章名(rd_book_chapter_ready 不触发扫描),首屏/未解析时用文件名,
* 避免开书即整书扫描占 UI 线程。 */
if (!g_book) { g_chapter[0] = 0; return; }
if (rd_book_chapter_ready(g_book) && !rd_book_is_image(g_book)) {
int page = rd_book_current(g_book);
int idx = -1;
if (rd_book_chapter_for_page(g_book, page, &idx) == 0 && idx >= 0) {
char t[RDCHAP_TITLE_MAX];
if (rd_book_chapter(g_book, idx, t, sizeof t, NULL, NULL, NULL, NULL) == 0) {
snprintf(g_chapter, sizeof g_chapter, "%s", t);
return;
}
}
}
snprintf(g_chapter, sizeof g_chapter, "%s", rd_book_basename(g_book));
}
@@ -176,6 +199,7 @@ static void reader_reflow(void)
{
if (!g_book) return;
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } /* 重排/雷字号时终止翻页动画 */
g_chap_cache_idx = -1; /* 页索引随字号/行距变 → 章范围缓存失效 */
int seg = 0; int32_t ch = 0;
rd_book_anchor(g_book, &seg, &ch); /* 记录当前锚,重排后回锚 */
g_prog_dirty = 1;
@@ -266,6 +290,354 @@ static void reader_draw_scrub(const meow_theme *th)
}
}
/* ---------------- R3.3 跳转弹层(章节模式 / 文件·段模式) ----------------
* 复用 g_dir 系列「半透明底 + 条目列表 + 当前高亮」;顶部提供:当前章节/段信息、页码输入
* (自绘数字键盘 0-9 + ⌫ + 跳)、快捷(首 / ±10 / ±100 / 末)。只对 TXT(seg 模式)启用:
* rd_book_chapter_n>0 → 章节模式;否则 rd_book_seg_n>0 → 文件/段模式。 */
#define J_SHORTCUT_N 6
#define J_SHORTCUT_X0 16
#define J_SHORTCUT_GAP 6
#define J_SHORTCUT_H 32
#define J_KEY_W 64
#define J_KEY_H 42
#define J_KEY_GAP 8
#define J_ROW 30
enum {
J_NONE = 0,
J_GO_FIRST = 1, J_GO_M100 = 2, J_GO_M10 = 3,
J_GO_P10 = 4, J_GO_P100 = 5, J_GO_LAST = 6,
J_DIG_0 = 10, J_DIG_1 = 11, J_DIG_2 = 12, J_DIG_3 = 13, J_DIG_4 = 14,
J_DIG_5 = 15, J_DIG_6 = 16, J_DIG_7 = 17, J_DIG_8 = 18, J_DIG_9 = 19,
J_BACK = 30, J_JUMP = 31,
J_ITEM = 100, /* 100 + i = 列表第 i 项 */
};
static int jump_hdr_y(void) { return g_top; }
static int jump_info_y(void) { return g_top + 30; }
static int jump_shortcut_y(void){ return g_top + 56; }
static int jump_input_y(void) { return g_top + 92; }
static int jump_pad_y(void) { return g_top + 128; }
static int jump_list_y(void) { return g_top + 128 + 4 * (J_KEY_H + J_KEY_GAP) + 10; }
static int chapter_range(int idx, int *start, int *end)
{
if (!g_book || idx < 0) return -1;
if (g_chap_cache_idx == idx) { if (start) *start = g_chap_cache_start; if (end) *end = g_chap_cache_end; return 0; }
int64_t co = 0;
if (rd_book_chapter(g_book, idx, NULL, 0, &co, NULL, NULL, NULL) != 0) return -1;
int s = rd_book_page_for_char(g_book, co);
if (s < 0) return -1;
int n = rd_book_chapter_n(g_book);
int e;
if (idx + 1 < n) {
int64_t co2 = 0;
if (rd_book_chapter(g_book, idx + 1, NULL, 0, &co2, NULL, NULL, NULL) != 0) return -1;
e = rd_book_page_for_char(g_book, co2) - 1;
} else {
e = rd_book_pages(g_book) - 1;
}
if (e < s) e = s;
g_chap_cache_idx = idx; g_chap_cache_start = s; g_chap_cache_end = e;
if (start) *start = s;
if (end) *end = e;
return 0;
}
/* 跳转选中项 idx 的页区间(章节模式=章范围;文件模式=段范围)。 */
static int jump_region(int idx, int *start, int *end, int *inp, int *ofp)
{
if (!g_book) return -1;
int s, e;
if (g_jump_mode == 1) {
if (chapter_range(idx, &s, &e) != 0) return -1;
} else if (g_jump_mode == 2) {
int nseg = rd_book_seg_n(g_book);
if (idx < 0 || idx >= nseg) return -1;
s = rd_book_seg_page(g_book, idx);
if (s < 0) return -1;
e = s + rd_book_seg_page_count(g_book, idx) - 1;
if (e < s) e = s;
} else return -1;
int cur = rd_book_current(g_book);
if (cur < s) cur = s;
if (cur > e) cur = e;
if (start) *start = s;
if (end) *end = e;
if (inp) *inp = cur - s + 1;
if (ofp) *ofp = e - s + 1;
return 0;
}
/* 页底条右侧文本:章内 y/Z · 全书 x/M(章节就绪且范围可算时),否则 全书 x/M。 */
static void footer_progress(char *out, int outsz)
{
int np = rd_book_pages(g_book);
int page = rd_book_current(g_book);
if (np <= 0) { snprintf(out, outsz, "0/0"); return; }
if (rd_book_chapter_ready(g_book) && !rd_book_is_image(g_book)) {
int idx = -1;
if (rd_book_chapter_for_page(g_book, page, &idx) == 0 && idx >= 0) {
int s = -1, e = -1;
if (chapter_range(idx, &s, &e) == 0 && e >= s) {
int inp = page - s + 1, ofp = e - s + 1;
if (inp > 0 && ofp > 0) { snprintf(out, outsz, "%d/%d · %d/%d", inp, ofp, page + 1, np); return; }
}
}
}
snprintf(out, outsz, "%d/%d", page + 1, np);
}
static void jump_chip(int x, int y, int w, int h, const char *label, int active, const meow_theme *th)
{
if (active) meow_ui_fill_rect(x, y, w, h, th->primaryContainer);
else meow_ui_fill_rect(x, y, w, h, th->surfaceContainerHighest);
uint32_t tc = active ? th->onPrimaryContainer : th->fg;
meow_ui_text_center(x + w / 2, y + (h - 20) / 2, label, 20, tc);
}
static void reader_draw_jump(const meow_theme *th)
{
if (!g_book) return;
int page = rd_book_current(g_book);
int np = rd_book_pages(g_book);
int n = (g_jump_mode == 1) ? rd_book_chapter_n(g_book) : rd_book_seg_n(g_book);
meow_ui_fill_rect_alpha(0, g_top, g_pw, g_ph - g_top, 0x000000, 150);
/* 头栏 */
int y0 = jump_hdr_y();
meow_ui_fill_rect(0, y0, g_pw, 30, th->surfaceContainer);
char hdr[48];
snprintf(hdr, sizeof hdr, "跳转 · %s", (g_jump_mode == 1) ? "章节" : "文件/段");
meow_ui_text(16, y0 + 5, hdr, 20, th->onSurface);
meow_ui_draw_rect(0, y0 + 29, g_pw, 1, th->outlineVariant);
if (n <= 0) {
meow_ui_text(20, jump_info_y() + 12, "本书无可用跳转目标", 20, th->dim);
meow_ui_text(20, g_ph - 30, "点空白关闭", 16, th->dim);
return;
}
/* 信息行 */
char info[256]; info[0] = 0;
int s, e, inp, ofp;
if (g_jump_mode == 1) {
char lab[RDCHAP_TITLE_MAX];
int num = -1;
if (rd_book_chapter(g_book, g_jump_sel, lab, sizeof lab, NULL, NULL, NULL, &num) == 0 &&
jump_region(g_jump_sel, &s, &e, &inp, &ofp) == 0) {
char prefix[24]; prefix[0] = 0;
if (num >= 0) snprintf(prefix, sizeof prefix, "第%d章 ", num);
snprintf(info, sizeof info, "%s%s · 章内 %d/%d · 全书 %d/%d", prefix, lab, inp, ofp, page + 1, np);
}
} else {
if (jump_region(g_jump_sel, &s, &e, &inp, &ofp) == 0)
snprintf(info, sizeof info, "段 %d/%d · 段内 %d/%d · 全书 %d/%d", g_jump_sel + 1, n, inp, ofp, page + 1, np);
}
meow_ui_text(16, jump_info_y() + 3, info[0] ? info : "—", 16, th->fg);
/* 快捷 chips */
static const char *sc_lbl[J_SHORTCUT_N] = { "首", "-100", "-10", "+10", "+100", "末" };
int x = J_SHORTCUT_X0;
int cw = (g_pw - 2 * J_SHORTCUT_X0 - (J_SHORTCUT_N - 1) * J_SHORTCUT_GAP) / J_SHORTCUT_N;
for (int i = 0; i < J_SHORTCUT_N; i++) {
jump_chip(x, jump_shortcut_y(), cw, J_SHORTCUT_H, sc_lbl[i], 0, th);
x += cw + J_SHORTCUT_GAP;
}
/* 输入行 */
meow_ui_text(16, jump_input_y() + 5, (g_jump_mode == 1) ? "章内页" : "段内页", 20, th->fg);
char ib[48];
snprintf(ib, sizeof ib, "%s", g_jump_buf[0] ? g_jump_buf : "_");
meow_ui_text(120, jump_input_y() + 3, ib, 24, th->primary);
meow_ui_text_right(g_pw - 16, jump_input_y() + 7, "输页码点「跳」 · ⌫退格", 16, th->dim);
/* 数字键盘 1..9 / ⌫ 0 跳 */
static const char *pad[4][3] = {
{ "1", "2", "3" }, { "4", "5", "6" }, { "7", "8", "9" }, { "⌫", "0", "跳" },
};
int pad_w = 3 * J_KEY_W + 2 * J_KEY_GAP;
int pad_x = (g_pw - pad_w) / 2;
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 3; c++) {
int kx = pad_x + c * (J_KEY_W + J_KEY_GAP);
int ky = jump_pad_y() + r * (J_KEY_H + J_KEY_GAP);
jump_chip(kx, ky, J_KEY_W, J_KEY_H, pad[r][c], (r == 3 && c == 2), th);
}
}
/* 条目列表(当前选中高亮) */
int top = jump_list_y();
int maxshow = (g_ph - top - 8) / J_ROW;
if (maxshow < 1) maxshow = 1;
/* 让当前选中项进入可视区 */
if (g_jump_sel >= 0) {
if (g_jump_sel < g_jump_scroll) g_jump_scroll = g_jump_sel;
else if (g_jump_sel >= g_jump_scroll + maxshow) g_jump_scroll = g_jump_sel - maxshow + 1;
}
if (g_jump_scroll + maxshow > n) g_jump_scroll = (n - maxshow < 0 ? 0 : n - maxshow);
if (g_jump_scroll < 0) g_jump_scroll = 0;
for (int i = 0; i < maxshow; i++) {
int item = g_jump_scroll + i;
if (item >= n) break;
char t[96];
if (g_jump_mode == 1) {
if (rd_book_chapter(g_book, item, t, sizeof t, NULL, NULL, NULL, NULL) != 0) continue;
} else {
snprintf(t, sizeof t, "段 %d", item + 1);
}
int yy = top + i * J_ROW;
if (item == g_jump_sel) meow_ui_fill_rect(0, yy, g_pw, J_ROW - 4, th->surfaceContainer);
meow_ui_text(24, yy + 4, t, 20, th->fg);
meow_ui_draw_rect(12, yy + J_ROW - 6, g_pw - 24, 1, th->outline);
}
meow_ui_text(20, g_ph - 30, "点条目跳转 · 点空白关闭", 16, th->dim);
}
static int reader_jump_hit(int x, int y)
{
int n = (g_jump_mode == 1) ? rd_book_chapter_n(g_book) : rd_book_seg_n(g_book);
if (n <= 0) return J_NONE;
/* 快捷 chips */
int sy = jump_shortcut_y();
if (y >= sy && y < sy + J_SHORTCUT_H) {
int cw = (g_pw - 2 * J_SHORTCUT_X0 - (J_SHORTCUT_N - 1) * J_SHORTCUT_GAP) / J_SHORTCUT_N;
if (x >= J_SHORTCUT_X0) {
int k = (x - J_SHORTCUT_X0) / (cw + J_SHORTCUT_GAP);
if (k >= 0 && k < J_SHORTCUT_N) return J_GO_FIRST + k;
}
return J_NONE;
}
/* 键盘 */
int py = jump_pad_y();
int pad_w = 3 * J_KEY_W + 2 * J_KEY_GAP;
int pad_x = (g_pw - pad_w) / 2;
for (int r = 0; r < 4; r++) {
int ky = py + r * (J_KEY_H + J_KEY_GAP);
if (y >= ky && y < ky + J_KEY_H) {
for (int c = 0; c < 3; c++) {
int kx = pad_x + c * (J_KEY_W + J_KEY_GAP);
if (x >= kx && x < kx + J_KEY_W) {
if (r == 3) {
if (c == 2) return J_JUMP;
if (c == 0) return J_BACK;
return J_DIG_0; /* 第三行 c==1 → 数字 0 */
}
return J_DIG_0 + (r * 3 + c) + 1; /* 0..2 行:1 2 3 / 4 5 6 / 7 8 9 */
}
}
return J_NONE;
}
}
/* 条目列表 */
int top = jump_list_y();
if (y >= top) {
int idx = (y - top) / J_ROW + g_jump_scroll;
if (idx < n) return J_ITEM + idx;
return J_NONE;
}
return J_NONE;
}
static void reader_jump_to(int page)
{
if (!g_book) return;
int np = rd_book_pages(g_book);
if (np <= 0) return;
int cur = rd_book_current(g_book);
(void)cur;
rd_book_set_page(g_book, page);
g_prog_dirty = 1;
reader_refill_page();
reader_redraw();
reader_reflow_persist();
}
static void reader_jump_action(int a)
{
if (a == J_NONE) { g_jump_open = 0; reader_redraw(); return; }
int s, e, inp, ofp;
if (a >= J_GO_FIRST && a <= J_GO_LAST) {
if (jump_region(g_jump_sel, &s, &e, &inp, &ofp) != 0) return;
int target = rd_book_current(g_book);
switch (a) {
case J_GO_FIRST: target = s; break;
case J_GO_M100: target -= 100; break;
case J_GO_M10: target -= 10; break;
case J_GO_P10: target += 10; break;
case J_GO_P100: target += 100; break;
case J_GO_LAST: target = e; break;
}
if (target < s) target = s;
if (target > e) target = e;
reader_jump_to(target);
return;
}
if (a >= J_DIG_0 && a <= J_DIG_9) {
size_t len = strlen(g_jump_buf);
if (len + 1 < sizeof g_jump_buf) { g_jump_buf[len] = (char)('0' + (a - J_DIG_0)); g_jump_buf[len + 1] = 0; }
reader_redraw();
return;
}
if (a == J_BACK) {
size_t len = strlen(g_jump_buf);
if (len > 0) g_jump_buf[len - 1] = 0;
reader_redraw();
return;
}
if (a == J_JUMP) {
if (g_jump_buf[0]) {
int tpage = atoi(g_jump_buf);
if (tpage < 1) tpage = 1;
if (jump_region(g_jump_sel, &s, &e, &inp, &ofp) == 0) {
int t = s + (tpage - 1);
if (t < s) t = s;
if (t > e) t = e;
reader_jump_to(t);
}
}
return;
}
if (a >= J_ITEM) {
int item = a - J_ITEM;
if (g_jump_mode == 1) {
if (jump_region(item, &s, &e, &inp, &ofp) == 0) {
g_jump_sel = item;
reader_jump_to(s);
}
} else {
int sp = rd_book_seg_page(g_book, item);
if (sp >= 0) { g_jump_sel = item; reader_jump_to(sp); }
}
return;
}
g_jump_open = 0; reader_redraw();
}
/* 打开跳转弹层:解析章节(首次会扫描+缓存),按是否有章节选章节/文件·段模式。 */
static void reader_jump_open(void)
{
if (!g_book) return;
g_jump_buf[0] = 0;
g_jump_scroll = 0;
int nc = rd_book_chapter_n(g_book);
if (nc > 0) {
g_jump_mode = 1;
int idx = -1;
rd_book_chapter_for_page(g_book, rd_book_current(g_book), &idx);
g_jump_sel = (idx >= 0) ? idx : 0;
} else if (rd_book_seg_n(g_book) > 0) {
g_jump_mode = 2;
int seg = rd_book_seg_of_page(g_book, rd_book_current(g_book));
g_jump_sel = (seg >= 0) ? seg : 0;
} else {
g_jump_mode = 2;
g_jump_sel = 0;
}
g_jump_open = 1;
g_menu_open = 0;
reader_redraw();
}
/* ----------------「界面」排版面板(B:字号/行距/背景/顶底栏/编码) ----------------
* 自绘 + 自制命中(README §5.2 建议:W_CUSTOM 画布 + 自制 hit,零 SDK widget 协同)。
* 几何 draw/hit 共用(常量)。 */
@@ -416,6 +788,7 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
{
(void)w; (void)ctx;
meow_ui_clear(g_page_bg);
chapter_name(); /* 每帧同步顶栏/页底章节名(就绪后取章名,否则文件名) */
if (g_book) {
int page = rd_book_current(g_book);
if ((g_has_font || rd_book_is_image(g_book)) && g_page_buf[RD_PAGE_SLOT_CUR]) {
@@ -427,8 +800,6 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
}
/* 页头条(书名 + 章节名;可选,rd.header) */
int sbh = meow_ui_statusbar_height();
int pages = g_book ? rd_book_pages(g_book) : 0;
int page = g_book ? rd_book_current(g_book) : 0;
const char *bk = g_book ? rd_book_basename(g_book) : "阅读";
if (g_show_header) {
meow_ui_fill_rect(0, sbh, g_pw, RD_HEADER_H, g_page_bg);
@@ -438,24 +809,23 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
}
/* 页底条(章节 + 页码;可选,rd.footer) */
if (g_show_footer) {
char ft[48];
if (pages > 0) snprintf(ft, sizeof ft, "%d/%d", page + 1, pages);
else snprintf(ft, sizeof ft, "0/0");
char ft[64];
footer_progress(ft, sizeof ft);
int fy = g_ph - RD_FOOT_H;
meow_ui_fill_rect(0, fy, g_pw, RD_FOOT_H, g_page_bg);
meow_ui_text(16, fy + 1, g_chapter[0] ? g_chapter : bk, 16, g_page_fg);
meow_ui_text_right(g_pw - 16, fy + 1, ft, 16, g_page_fg);
}
/* R3 刮擦指示条(非目录/面板覆盖时显示;菜单为底部条,与右缘条并存即可) */
if (g_book && !g_dir_open && !g_ui_open) reader_draw_scrub(th);
/* R3 刮擦指示条(非目录/面板/跳转覆盖时显示;菜单为底部条,与右缘条并存即可) */
if (g_book && !g_dir_open && !g_ui_open && !g_jump_open) reader_draw_scrub(th);
if (g_menu_open) {
int y = g_ph - RD_MENU_H;
meow_ui_fill_rect_alpha(0, y, g_pw, RD_MENU_H, 0x000000, 160);
/* 4 按钮(MVP 单条):返回 | 目录 | 界面 | 设置 */
/* 5 按钮:返回 | 目录 | 跳转 | 界面 | 设置 */
int bw = g_pw / RD_MENU_BTN;
const char *labels[RD_MENU_BTN] = { "返回", "目录", "界面", "设置" };
const char *labels[RD_MENU_BTN] = { "返回", "目录", "跳转", "界面", "设置" };
for (int i = 0; i < RD_MENU_BTN; i++) {
int bx = i * bw;
meow_ui_fill_rect(bx + 4, y + 12, bw - 8, RD_MENU_H - 24, th->surfaceContainer);
@@ -463,9 +833,13 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
}
}
if (g_ui_open && g_book) reader_draw_ui_panel(th);
if (g_jump_open && g_book) reader_draw_jump(th);
if (g_dir_open && g_book) {
/* 目录覆盖层:半透明底 + 条目列表(当前行高亮) */
int n = rd_book_toc_n(g_book);
/* 目录覆盖层:TXT 有章节 → 章节列表(复用 rd_book 章节数据,与跳转共享);否则 rdsrc 目录;无则「无章节」。 */
int nchap = 0;
if (!rd_book_is_image(g_book) && rd_book_chapter_n(g_book) > 0) nchap = rd_book_chapter_n(g_book);
int ntoc = (nchap == 0) ? rd_book_toc_n(g_book) : 0;
int n = (nchap > 0) ? nchap : ntoc;
if (n > 0) {
int top = g_top + 8;
int row = 34;
@@ -474,19 +848,29 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
if (maxshow > n) maxshow = n;
g_dir_top = top; g_dir_row_h = row;
meow_ui_fill_rect_alpha(0, g_top, g_pw, g_ph - g_top, 0x000000, 150);
char t[96]; int sg; int32_t cch;
char t[160];
int page = rd_book_current(g_book);
int curchap = -1;
if (nchap > 0) rd_book_chapter_for_page(g_book, page, &curchap);
for (int i = 0; i < maxshow; i++) {
if (rd_book_toc(g_book, i, t, sizeof t, &sg, &cch) != 0) continue;
int hl = 0;
if (nchap > 0) {
if (rd_book_chapter(g_book, i, t, sizeof t, NULL, NULL, NULL, NULL) != 0) continue;
hl = (i == curchap);
} else {
int sg = -1; int32_t cch = 0;
if (rd_book_toc(g_book, i, t, sizeof t, &sg, &cch) != 0) continue;
hl = (page == sg);
}
int y0 = top + i * row;
if (page == sg) meow_ui_fill_rect(0, y0, g_pw, row - 4, th->surfaceContainer);
if (hl) meow_ui_fill_rect(0, y0, g_pw, row - 4, th->surfaceContainer);
meow_ui_text(24, y0, t, 20, th->fg);
meow_ui_draw_rect(12, y0 + row - 6, g_pw - 24, 1, th->outline);
}
meow_ui_text(20, g_ph - 34, "点条目跳转 · 点空白关闭", 16, th->dim);
} else {
g_dir_open = 0;
meow_ui_text(20, g_top + 40, "本书无目录", 20, th->dim);
meow_ui_text(20, g_top + 40, (rd_book_chapter_n(g_book) == 0) ? "本书无章节" : "本书无目录", 20, th->dim);
}
}
}
@@ -629,12 +1013,13 @@ static void reader_scrub_preview(int page)
static void reader_scrub_begin(int x, int y)
{
(void)x;
if (!g_book || g_menu_open || g_dir_open) return;
if (!g_book || g_menu_open || g_dir_open || g_jump_open) return;
if (g_pw - x > RD_SCRUB_W) return;
if (y < rect_y0() || y >= rect_y1()) return;
int np = rd_book_pages(g_book);
if (np <= 1) return;
g_scrub = 1;
g_scrub_moved = 0; /* 每次触控重置;拖动(touch_move)才置 1,区分点按/刮擦 */
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; }
reader_scrub_preview(scrub_page_for_y(y));
}
@@ -710,9 +1095,10 @@ int reader_open(const char *path)
reader_alloc_page_bufs(); /* 重建 cur+next 两槽;g_anim_old 翻页时懒分配 */
}
/* 复位动画/刮擦状态(跨书) */
/* 复位动画/刮擦/覆盖层状态(跨书) */
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; }
g_scrub = 0; g_menu_open = 0; g_dir_open = 0; g_ui_open = 0;
g_scrub = 0; g_menu_open = 0; g_dir_open = 0; g_ui_open = 0; g_jump_open = 0;
g_chap_cache_idx = -1;
/* 编码:初始 = 打开探测结果;进度里的显式编码(Big5 用户选定)优先重开 */
if (g_book && !rd_book_is_image(g_book)) {
@@ -739,6 +1125,23 @@ int reader_open(const char *path)
return 0;
}
/* ---------- R3.4 章节重解析(app/settings.c 调用) ---------- */
int reader_has_chapter_book(void)
{
if (!g_book || rd_book_is_image(g_book)) return 0;
/* TXT + 分段流式(seg 模式)即支持章节解析;用 rd_book_seg_n(不触发扫描)判定。 */
return (rd_book_seg_n(g_book) > 0) ? 1 : 0;
}
int reader_reparse_chapters(void)
{
if (!g_book) return -1;
int rc = rd_book_reparse_chapters(g_book);
if (rc >= 0) g_chap_cache_idx = -1; /* 章节列表变化 → 章范围缓存失效 */
if (g_reader_active) reader_redraw();
return rc;
}
/* ---------- 视图生命周期 ---------- */
static void reader_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; }
static void reader_on_enter(meow_view *v, void *ctx)
@@ -766,7 +1169,8 @@ static void reader_on_destroy(meow_view *v, void *ctx)
if (g_book) rd_book_close(g_book);
g_book = NULL;
reader_free_page_bufs();
g_menu_open = 0; g_dir_open = 0; g_ui_open = 0; g_scrub = 0; g_reader_active = 0;
g_menu_open = 0; g_dir_open = 0; g_ui_open = 0; g_jump_open = 0; g_scrub = 0; g_reader_active = 0;
g_chap_cache_idx = -1;
}
meow_view *reader_view_create(void)
@@ -785,28 +1189,52 @@ void reader_on_tap(int x, int y)
{
(void)x;
if (!g_book) { return; }
if (g_scrub) { /* 刮擦抬手 → 跳到目标页收尾 */
if (g_scrub) { /* 刮擦抬手 → 跳到目标页收尾(仅真实拖动过才跳;点按右缘=翻下一页) */
g_scrub = 0;
reader_scrub_jump(g_scrub_page);
if (g_scrub_moved) { reader_scrub_jump(g_scrub_page); return; }
/* 未拖动:当作点按,落到下方三区翻页逻辑(右 1/3 下一页),避免右缘误触跳远页 */
}
if (g_jump_open) {
/* 跳转弹层:快捷/键盘/条目 → 执行;点空白关闭 */
int a = reader_jump_hit(x, y);
reader_jump_action(a);
return;
}
if (g_dir_open) {
/* 目录覆盖层:点条目跳转,点空白关闭 */
int n = rd_book_toc_n(g_book);
/* 目录覆盖层:TXT 章节(复用 rd_book 章节数据)或 rdsrc 目录;点条目跳转,点空白关闭 */
int nchap = 0;
if (!rd_book_is_image(g_book) && rd_book_chapter_n(g_book) > 0) nchap = rd_book_chapter_n(g_book);
int n = (nchap > 0) ? nchap : rd_book_toc_n(g_book);
if (n > 0 && x >= 0 && y >= g_dir_top) {
int row = g_dir_row_h > 0 ? g_dir_row_h : 34;
int idx = (y - g_dir_top) / row;
if (idx < n && idx >= 0) {
char t[96]; int sg; int32_t cch;
if (rd_book_toc(g_book, idx, t, sizeof t, &sg, &cch) == 0) {
if (g_book && sg >= 0) {
rd_book_jump_to_anchor(g_book, sg, cch, g_fs, g_ls);
g_dir_open = 0; g_menu_open = 0;
snprintf(g_chapter, sizeof g_chapter, "%s", t);
reader_refill_page();
reader_redraw();
reader_reflow_persist();
return;
if (nchap > 0) {
int64_t co = 0;
if (rd_book_chapter(g_book, idx, NULL, 0, &co, NULL, NULL, NULL) == 0) {
int sp = rd_book_page_for_char(g_book, co);
if (sp >= 0) {
rd_book_set_page(g_book, sp);
g_dir_open = 0; g_menu_open = 0;
g_chap_cache_idx = -1;
reader_refill_page();
reader_redraw();
reader_reflow_persist();
return;
}
}
} else {
char t[96]; int sg; int32_t cch;
if (rd_book_toc(g_book, idx, t, sizeof t, &sg, &cch) == 0) {
if (g_book && sg >= 0) {
rd_book_jump_to_anchor(g_book, sg, cch, g_fs, g_ls);
g_dir_open = 0; g_menu_open = 0;
snprintf(g_chapter, sizeof g_chapter, "%s", t);
reader_refill_page();
reader_redraw();
reader_reflow_persist();
return;
}
}
}
}
@@ -865,14 +1293,18 @@ void reader_on_tap(int x, int y)
reader_reflow_persist();
meow_view_page_pop();
return;
case 1: /* 目录 */
if (rd_book_toc_n(g_book) > 0) { g_dir_open = 1; g_menu_open = 0; reader_redraw(); }
case 1: /* 目录(TXT 章节 → 章节列表;无章节显示「本书无章节」) */
g_dir_open = 1; g_menu_open = 0; reader_redraw();
return;
case 2: /* 跳转(章节/文件·段模式) */
if (rd_book_chapter_n(g_book) > 0 || rd_book_seg_n(g_book) > 0) { reader_jump_open(); }
else if (rd_book_toc_n(g_book) > 0) { g_dir_open = 1; g_menu_open = 0; reader_redraw(); }
else { g_menu_open = 0; reader_redraw(); }
return;
case 2: /* 界面(阅读排版设置) */
case 3: /* 界面(阅读排版设置) */
g_ui_open = 1; g_menu_open = 0; reader_redraw();
return;
case 3: /* 设置 → App 设置页 */
case 4: /* 设置 → App 设置页 */
reader_reflow_persist();
meow_view_page_push(ACT_SETTINGS);
return;
@@ -901,6 +1333,7 @@ void reader_on_swipe(int dir, int dx, int dy)
* 刮擦(reader_scrub_begin,由 touch_down/move 驱动)两条既有路径。 */
if (g_ui_open) { g_ui_open = 0; reader_redraw(); return; }
if (g_dir_open) { g_dir_open = 0; reader_redraw(); return; }
if (g_jump_open) { g_jump_open = 0; reader_redraw(); return; }
if (g_menu_open) { g_menu_open = 0; reader_redraw(); return; }
if (dir == MEOW_SWIPE_LEFT || dir == MEOW_SWIPE_RIGHT) {
reader_reflow_persist();
@@ -913,7 +1346,7 @@ void reader_on_touch_down(int x, int y) { reader_scrub_begin(x, y); }
void reader_on_touch_move(int x, int y)
{
(void)x;
if (g_scrub && g_book) reader_scrub_preview(scrub_page_for_y(y));
if (g_scrub && g_book) { g_scrub_moved = 1; reader_scrub_preview(scrub_page_for_y(y)); }
}
/* 系统翻页信号(E):reader_ipc 线程收到 MSG_KEY_PAGEUP/PAGEDOWN 后置 pending 计数;
@@ -932,9 +1365,12 @@ void reader_on_tick(uint64_t now)
if (g_volpage_pending) {
int dir = g_volpage_pending > 0 ? 1 : -1;
g_volpage_pending = 0;
if (g_book && !g_menu_open && !g_dir_open && !g_ui_open)
if (g_book && !g_menu_open && !g_dir_open && !g_ui_open && !g_jump_open)
reader_turn_animated(dir);
}
/* Phase 2C:空闲预取下一段(距段界 ≤2% 时提前解码进窗口,翻段界零突发) */
if (g_book && !g_menu_open && !g_dir_open && !g_ui_open && !g_jump_open)
rd_book_prefetch(g_book);
/* R3.2:进度实时落盘(翻页/刮擦/字号/编码变更后,节流 ~1s 定时写)——意外崩溃重开不丢进度 */
if (g_prog_dirty && now - g_prog_last_save_ms >= 1000) {
g_prog_last_save_ms = now;
+38 -8
View File
@@ -20,10 +20,16 @@ static meow_widget *g_appbar;
static meow_widget *g_list;
static int g_active;
/* 行项枚举(顺序 = 列表顺序) */
enum { ITEM_FS = 0, ITEM_LS, ITEM_BG, ITEM_THEME, ITEM_SHELF, ITEM_HDR, ITEM_FTR, ITEM_COUNT };
/* 行项枚举(顺序 = 列表顺序)。ITEM_BASE = 固定的全局项数;重解析本书章节仅在
* 已打开一本「支持章节解析」(TXT+seg 模式)的书时追加(见 settings_on_enter)。 */
enum {
ITEM_FS = 0, ITEM_LS, ITEM_BG, ITEM_THEME, ITEM_SHELF, ITEM_HDR, ITEM_FTR,
ITEM_REPARSE, ITEM_COUNT
};
#define ITEM_BASE ITEM_REPARSE /* 7:不含重解析的固定项数 */
static int g_nitems = ITEM_BASE;
static const char *const g_labels[ITEM_COUNT] = {
"默认字号", "默认行距", "默认背景", "主题", "书架布局", "页头条", "页底条"
"默认字号", "默认行距", "默认背景", "主题", "书架布局", "页头条", "页底条", "重解析本书章节"
};
static void settings_redraw(void);
@@ -61,6 +67,7 @@ static void item_value(int item, char *out, int outsz)
case ITEM_SHELF: snprintf(out, (size_t)outsz, "%s", shelf_label(c)); break;
case ITEM_HDR: snprintf(out, (size_t)outsz, "%s", c->header ? "显示" : "隐藏"); break;
case ITEM_FTR: snprintf(out, (size_t)outsz, "%s", c->footer ? "显示" : "隐藏"); break;
case ITEM_REPARSE: snprintf(out, (size_t)outsz, "%s", "点按重新扫描章节"); break;
default: snprintf(out, (size_t)outsz, "-"); break;
}
}
@@ -69,15 +76,15 @@ static void item_value(int item, char *out, int outsz)
static int settings_src(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= ITEM_COUNT) return 0;
static char valbuf[16];
if (idx < 0 || idx >= g_nitems || idx >= ITEM_COUNT) return 0;
static char valbuf[48];
item_value(idx, valbuf, sizeof valbuf);
out->icon_kind = MEOW_LIST_ICON_NONE;
out->title = g_labels[idx];
out->subtitle = valbuf;
out->right = MEOW_LIST_RIGHT_ARROW;
out->on = 0;
out->enabled = 1;
out->enabled = (idx == ITEM_REPARSE) ? reader_has_chapter_book() : 1;
return 1;
}
@@ -147,6 +154,7 @@ static void apply_choice(int item, int sel)
/* 打开一个 SELECT 对话框并应用。 */
static void settings_open_choice(int item)
{
if (item == ITEM_REPARSE) return; /* 重解析走 settings_reparse(非 SELECT) */
const rdconf *c = rdconf_get();
if (!c) return;
const char *title = g_labels[item];
@@ -191,6 +199,25 @@ static void settings_redraw(void)
meow_ui_flush();
}
/* R3.4 «重解析本书章节»:确认 → 清缓存+重扫 → 结果对话框(成功/失败/进行中=确认弹窗)。 */
static void settings_reparse(void)
{
meow_dialog *d = meow_dialog_create(MEOW_DLG_CONFIRM, "重解析本书章节?\n将清除章节缓存并重新扫描全书。");
if (!d) return;
meow_dialog_show(d);
int ok = meow_dialog_result(d);
meow_dialog_free(d);
if (ok != 1) { settings_redraw(); return; } /* 取消 */
int rc = reader_reparse_chapters(); /* 阻塞:重扫全书段 */
char msg[120];
if (rc >= 0) snprintf(msg, sizeof msg, "重解析完成:共识别 %d 章(已保存缓存)", rc);
else if (rc == -2) snprintf(msg, sizeof msg, "重解析失败:扫描出错,已回退为无章节(未保存)");
else snprintf(msg, sizeof msg, "该书不支持章节解析");
meow_dialog *r = meow_dialog_create(MEOW_DLG_INFO, msg);
if (r) { meow_dialog_show(r); meow_dialog_result(r); meow_dialog_free(r); }
settings_redraw();
}
/* view 生命周期 */
static void settings_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; }
static void settings_on_enter(meow_view *v, void *ctx)
@@ -203,7 +230,9 @@ static void settings_on_enter(meow_view *v, void *ctx)
if (g_appbar) meow_topappbar_set_back(g_appbar, 1);
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
int ly = sbh + SET_APPBAR_H + 8;
g_list = meow_listview_create(SET_X, ly, SET_W, 720 - ly, ITEM_COUNT,
/* 仅当打开的书「支持章节解析」(TXT+seg)才显示「重解析本书章节」项 */
g_nitems = ITEM_BASE + (reader_has_chapter_book() ? 1 : 0);
g_list = meow_listview_create(SET_X, ly, SET_W, 720 - ly, g_nitems,
settings_src, NULL, MEOW_LIST_SEL_SINGLE);
if (g_list) {
meow_listview_set_container(g_list, 1);
@@ -240,7 +269,8 @@ int settings_on_tap(int x, int y)
}
if (g_list) {
int idx = meow_listview_tap_at(g_list, x, y);
if (idx >= 0 && idx < ITEM_COUNT) {
if (idx >= 0 && idx < g_nitems && idx < ITEM_COUNT) {
if (idx == ITEM_REPARSE) { settings_reparse(); return 1; }
settings_open_choice(idx);
return 1;
}
+7 -3
View File
@@ -95,6 +95,10 @@ echo "==> host seg test (R3.2)"
$HOST_CC -O2 -Wall -Wextra -Icore test/seg_smoke.c core/seg.c core/rdenc.c -o test/seg_smoke_host -lm
./test/seg_smoke_host
echo "==> host chapter test (R3.3)"
$HOST_CC -O2 -Wall -Wextra -Icore test/chapter_smoke.c core/rdchapter.c core/seg.c core/rdenc.c -o test/chapter_smoke_host -lm
./test/chapter_smoke_host
echo "==> host loaders test"
HOBJ="$OBJ/host"; mkdir -p "$HOBJ"
for f in vendor/miniz/miniz_tinfl.c vendor/miniz/miniz_tdef.c vendor/miniz/miniz_zip.c vendor/miniz/miniz.c; do
@@ -119,21 +123,21 @@ timeout 120 qemu-mipsel ./test/rdft_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-
# ---------- 6) qemu-mipsel 书桥冒烟(TXT + 四格式) ----------
echo "==> mipsel book smoke (TXT)"
$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC test/book_smoke.c \
app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \
app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdchapter.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \
core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c engine/rdft.c \
"$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -o test/book_smoke_mipsel
timeout 120 qemu-mipsel ./test/book_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}"
echo "==> mipsel formats smoke"
$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC test/formats_smoke.c \
app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \
app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdchapter.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \
core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c engine/rdft.c \
"$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -o test/formats_smoke_mipsel
timeout 150 qemu-mipsel ./test/formats_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}" /tmp/rd_samples
# ---------- 7) reader 二进制(mipsel 静态) ----------
echo "==> reader binary"
CORE_L="core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c"
CORE_L="core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdchapter.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c"
ENGINE_L="engine/rdft.c"
APP_L="app.c app/shelf.c app/reader.c app/book.c app/progress.c app/settings.c app/reader_conf.c app/reader_ipc.c"
$CC -Os -static -Wall -Wextra -I../sdk -I../meow_launcherd -I. -Icore -Iapp -Iengine $FT_INCS $COREINC \
+528
View File
@@ -0,0 +1,528 @@
/* core/rdchapter.c — TXT 章节规范解析器实现(见 core/rdchapter.h)。 */
#include "rdchapter.h"
#include "seg.h"
#include "rdtxt.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---------------- 基础字符工具 ---------------- */
static int ascii_digit_val(uint32_t cp)
{
return (cp >= '0' && cp <= '9') ? (int)(cp - '0') : -1;
}
/* CJK / 全角宽度 = 2,其余 = 1(近似;不含零宽组合符)。 */
static int cp_width(uint32_t cp)
{
if (cp == 0x3000 || cp == 0x3001 || cp == 0x3002) return 2;
if ((cp >= 0x2E80 && cp <= 0x303E) || (cp >= 0x3040 && cp <= 0x30FF) ||
(cp >= 0x3100 && cp <= 0x318F) || (cp >= 0x3400 && cp <= 0x4DBF) ||
(cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0xA000 && cp <= 0xA4CF) ||
(cp >= 0xAC00 && cp <= 0xD7A3) || (cp >= 0xF900 && cp <= 0xFAFF) ||
(cp >= 0xFE30 && cp <= 0xFE4F) || (cp >= 0xFF00 && cp <= 0xFF60) ||
(cp >= 0xFFE0 && cp <= 0xFFE6)) return 2;
return 1;
}
static int disp_width(const uint32_t *cps, int s, int e)
{
int w = 0;
for (int i = s; i < e; i++) w += cp_width(cps[i]);
return w;
}
/* 章节标记字符集:章 回 节 卷 集 部 篇。 */
static int is_marker(uint32_t cp)
{
return cp == 0x7AE0 || cp == 0x56DE || cp == 0x8282 ||
cp == 0x5377 || cp == 0x96C6 || cp == 0x90E8 || cp == 0x7BC7;
}
/* 卷 部 集(卷/集类章节前缀)。 */
static int is_volmark(uint32_t cp)
{
return cp == 0x5377 /* 卷 */ || cp == 0x90E8 /* 部 */ || cp == 0x96C6 /* 集 */;
}
/* 中文数字字符(含位权)。 */
static int is_cn_num_char(uint32_t cp)
{
return cp == 0x96F6 /* 零 */ || cp == 0x4E00 /* 一 */ || cp == 0x4E8C /* 二 */ ||
cp == 0x4E09 /* 三 */ || cp == 0x56DB /* 四 */ || cp == 0x4E94 /* 五 */ ||
cp == 0x516D /* 六 */ || cp == 0x4E03 /* 七 */ || cp == 0x516B /* 八 */ ||
cp == 0x4E5D /* 九 */ || cp == 0x5341 /* 十 */ || cp == 0x767E /* 百 */ ||
cp == 0x5343 /* 千 */ || cp == 0x4E07 /* 万 */ || cp == 0x4E24 /* 两 */;
}
static int cn_digit_val(uint32_t cp)
{
switch (cp) {
case 0x96F6: return 0; /* 零 */
case 0x4E00: return 1; /* 一 */
case 0x4E8C: return 2; /* 二 */
case 0x4E09: return 3; /* 三 */
case 0x56DB: return 4; /* 四 */
case 0x4E94: return 5; /* 五 */
case 0x516D: return 6; /* 六 */
case 0x4E03: return 7; /* 七 */
case 0x516B: return 8; /* 八 */
case 0x4E5D: return 9; /* 九 */
case 0x4E24: return 2; /* 两 */
default: return -1;
}
}
static int cn_weight_val(uint32_t cp)
{
switch (cp) {
case 0x5341: return 10; /* 十 */
case 0x767E: return 100; /* 百 */
case 0x5343: return 1000; /* 千 */
case 0x4E07: return 10000; /* 万 */
default: return 0;
}
}
/* 中文数字子串 [s,e) → 阿拉伯;非法/空返回 -1。实现:digit 累积 + 位权叠加。 */
static int cn_num_from_cps(const uint32_t *cps, int s, int e)
{
long total = 0, cur = 0, digit = 0;
int any = 0;
for (int i = s; i < e; i++) {
uint32_t cp = cps[i];
int dv = cn_digit_val(cp);
if (dv >= 0) { digit = dv; any = 1; continue; }
int w = cn_weight_val(cp);
if (w > 0) {
cur += (digit ? digit : 1) * (long)w;
digit = 0; any = 1;
continue;
}
return -1; /* 非中文数字字符 */
}
total += cur + digit;
return any ? (int)total : -1;
}
/* 行内正文标点计数:句号/逗号/顿号/分号/问号/感叹号/省略号 + ASCII . , ; ! ?
* 返回计数(区分「极少」:≤1 视为标题,>1 视为正文)。 */
static int sent_punct_count(const uint32_t *cps, int s, int e)
{
int n = 0;
for (int i = s; i < e; i++) {
uint32_t cp = cps[i];
if (cp == 0x3002 /* 。 */ || cp == 0xFF0C /* , */ || cp == 0x3001 /* 、 */ ||
cp == 0xFF1B /* ; */ || cp == 0xFF1F /* ? */ || cp == 0xFF01 /* ! */ ||
cp == 0x2026 /* … */ || cp == '.' || cp == ',' || cp == ';' ||
cp == '!' || cp == '?') n++;
}
return n;
}
/* cps[s..] 是否以 utf8 字面量为前缀(逐码点比较)。 */
static int cps_prefix(const uint32_t *cps, int s, int e, const uint8_t *utf8)
{
int i = s;
const uint8_t *p = utf8;
while (*p && i < e) {
uint32_t cp;
uint8_t b0 = *p;
int n;
if (b0 < 0x80) { cp = b0; n = 1; }
else if ((b0 & 0xE0) == 0xC0) { cp = ((uint32_t)(b0 & 0x1F) << 6) | (p[1] & 0x3F); n = 2; }
else if ((b0 & 0xF0) == 0xE0) { cp = ((uint32_t)(b0 & 0x0F) << 12) | ((uint32_t)(p[1] & 0x3F) << 6) | (p[2] & 0x3F); n = 3; }
else { cp = ((uint32_t)(b0 & 0x07) << 18) | ((uint32_t)(p[1] & 0x3F) << 12) | ((uint32_t)(p[2] & 0x3F) << 6) | (p[3] & 0x3F); n = 4; }
if (cps[i] != cp) return 0;
i++; p += n;
}
return (*p == 0) && (i > s);
}
/* ASCII 大小写无关前缀比较(用于 "chapter")。 */
static int cps_prefix_ci(const uint32_t *cps, int s, int e, const char *ascii)
{
int i = s;
for (const char *p = ascii; *p; p++) {
if (i >= e) return 0;
uint32_t cp = cps[i++];
uint32_t lo = (cp >= 'A' && cp <= 'Z') ? cp + 32 : cp;
char ch = *p;
if (lo != (uint32_t)ch) return 0;
}
return 1;
}
/* 罗马数字 → 阿拉伯(ivxl);解析失败 -1。 */
static int roman_to_int(const uint32_t *cps, int s, int e, int *end)
{
struct { uint32_t cp; int v; } tbl[] = {
{ 'i', 1 }, { 'v', 5 }, { 'x', 10 }, { 'l', 50 }, { 'c', 100 },
};
int vals[32], k = 0;
int i = s;
while (i < e && k < 32) {
uint32_t cp = cps[i];
if (cp >= 'A' && cp <= 'Z') cp += 32; /* 小写化 */
int v = 0;
for (int t = 0; t < 5; t++) if (tbl[t].cp == cp) { v = tbl[t].v; break; }
if (v == 0) break;
vals[k++] = v; i++;
}
if (k == 0) return -1;
int total = 0, prev = 0;
for (int j = k - 1; j >= 0; j--) {
int v = vals[j];
if (v < prev) total -= v; else total += v;
prev = v;
}
*end = i;
return total;
}
/* ---- 英文数字词(Chapter one / twenty-one / two hundred fifty ...)→ 阿拉伯 ---- */
static int cps_ascii_word_eq_ci(const uint32_t *cps, int s, int e, const char *w)
{
int len = (int)strlen(w);
if (e - s != len) return 0;
for (int k = 0; k < len; k++) {
uint32_t c = cps[s + k];
if (c >= 'A' && c <= 'Z') c += 32;
if (c != (uint32_t)(unsigned char)w[k]) return 0;
}
return 1;
}
/* 英文数字词一 → 值;不识别返回 -1。覆盖 one..nineteen、twenty..ninety、hundred、thousand。 */
static int en_word_val(const uint32_t *cps, int s, int e)
{
static const struct { const char *w; int v; } tbl[] = {
{ "one", 1 }, { "two", 2 }, { "three", 3 }, { "four", 4 }, { "five", 5 },
{ "six", 6 }, { "seven", 7 }, { "eight", 8 }, { "nine", 9 }, { "ten", 10 },
{ "eleven", 11 }, { "twelve", 12 }, { "thirteen", 13 }, { "fourteen", 14 },
{ "fifteen", 15 }, { "sixteen", 16 }, { "seventeen", 17 }, { "eighteen", 18 },
{ "nineteen", 19 }, { "twenty", 20 }, { "thirty", 30 }, { "forty", 40 },
{ "fifty", 50 }, { "sixty", 60 }, { "seventy", 70 }, { "eighty", 80 },
{ "ninety", 90 }, { "hundred", 100 }, { "thousand", 1000 },
};
for (int t = 0; t < (int)(sizeof tbl / sizeof tbl[0]); t++)
if (cps_ascii_word_eq_ci(cps, s, e, tbl[t].w)) return tbl[t].v;
return -1;
}
/* 解析一段英文数字词(one / twenty-one / two hundred fifty ...)。*end=末字符位置。
* 失败(无词/未知词/越界/数值越界)返回 -1。 */
static int word_num_from_cps(const uint32_t *cps, int s, int e, int *end)
{
long total = 0, cur = 0;
int i = s, words = 0, last_end = s;
while (i < e) {
while (i < e && (cps[i] == ' ' || cps[i] == '-' || cps[i] == 0x3000 || cps[i] == '\t')) i++;
int ws = i;
while (i < e && ((cps[i] >= 'a' && cps[i] <= 'z') || (cps[i] >= 'A' && cps[i] <= 'Z'))) i++;
if (i == ws) break;
int v = en_word_val(cps, ws, i);
if (v < 0) return -1;
if (v == 100 || v == 1000) { long base = (cur == 0) ? 1 : cur; total += base * (long)v; cur = 0; }
else cur += (long)v;
words++;
last_end = i;
}
if (words == 0) return -1;
long n = total + cur;
if (n <= 0 || n > 100000) return -1;
*end = last_end;
return (int)n;
}
/* 把 cps[s..e) 转成 UTF-8 到 out(有界)。 */
static void cps_to_utf8(const uint32_t *cps, int s, int e, char *out, int outsz)
{
int o = 0;
for (int i = s; i < e; i++) {
uint8_t b[4];
int n = rd_cp_to_utf8(cps[i], b);
if (o + n + 1 > outsz) break;
for (int k = 0; k < n; k++) out[o++] = (char)b[k];
}
out[o] = 0;
}
/* 特殊前缀章节(楔子/序章/序言/引子/前言/序幕/尾声/后记/番外/外传/终章/大结局/正文)。 */
static const uint8_t *const g_special[] = {
(const uint8_t *)"\xE6\xA5\x94\xE5\xAD\x90", /* 楔子 */
(const uint8_t *)"\xE5\xBA\x8F\xE7\xAB\xA0", /* 序章 */
(const uint8_t *)"\xE5\xBA\x8F\xE8\xA8\x80", /* 序言 */
(const uint8_t *)"\xE5\xBC\x95\xE5\xAD\x90", /* 引子 */
(const uint8_t *)"\xE5\x89\x8D\xE8\xA8\x80", /* 前言 */
(const uint8_t *)"\xE5\xBA\x8F\xE5\xB9\x95", /* 序幕 */
(const uint8_t *)"\xE5\xB0\xBE\xE5\xA3\xB0", /* 尾声 */
(const uint8_t *)"\xE5\x90\x8E\xE8\xAE\xB0", /* 后记 */
(const uint8_t *)"\xE7\x95\xAA\xE5\xA4\x96", /* 番外 */
(const uint8_t *)"\xE5\xA4\x96\xE4\xBC\xA0", /* 外传 */
(const uint8_t *)"\xE7\xBB\x88\xE7\xAB\xA0", /* 终章 */
(const uint8_t *)"\xE5\xA4\xA7\xE7\xBB\x93\xE5\xB1\x80", /* 大结局 */
(const uint8_t *)"\xE6\xAD\xA3\xE6\x96\x87", /* 正文 */
};
#define G_SPECIAL_N ((int)(sizeof g_special / sizeof g_special[0]))
/* 判断一行(cps[ls..le),ls=首个非空白字符)是否为章节标题行。
* 命中则填 title(完整行)与 *num;返回 1,否则 0。 */
static int chap_match_line(const uint32_t *cps, int ls, int le, char *title, int outsz, int *num)
{
if (ls >= le) return 0;
if (disp_width(cps, ls, le) > RDCHAP_DISP_WMAX) return 0;
/* A/B:`第` + 阿拉伯/中文数字 + 章回节卷集部篇 */
if (cps[ls] == 0x7B2C /* 第 */) {
int i = ls + 1;
int numv = -1;
int dstart = i;
while (i < le && ascii_digit_val(cps[i]) >= 0) i++;
if (i > dstart) {
if (i - dstart > 4) return 0;
numv = 0;
for (int k = dstart; k < i; k++) numv = numv * 10 + ascii_digit_val(cps[k]);
} else {
int cstart = i;
while (i < le && is_cn_num_char(cps[i])) i++;
if (i == cstart) return 0;
if (i - cstart > 8) return 0;
numv = cn_num_from_cps(cps, cstart, i);
if (numv < 0) return 0;
}
if (i >= le || !is_marker(cps[i])) return 0;
i++; /* 消费标记 */
if (sent_punct_count(cps, i, le) > 1) return 0;
cps_to_utf8(cps, ls, le, title, outsz);
*num = numv;
return 1;
}
/* C:阿拉伯数字 + 可选分隔符(、.. 空格 全角空格) + 章回节卷集部篇 */
if (ascii_digit_val(cps[ls]) >= 0) {
int i = ls, numv = 0, cnt = 0;
while (i < le && ascii_digit_val(cps[i]) >= 0) { numv = numv * 10 + ascii_digit_val(cps[i]); i++; cnt++; }
if (cnt < 1 || cnt > 4) return 0;
if (i < le && (cps[i] == 0x3001 || cps[i] == '.' || cps[i] == ' ' || cps[i] == 0x3000)) i++;
if (i >= le || !is_marker(cps[i])) return 0;
i++;
if (sent_punct_count(cps, i, le) > 1) return 0;
cps_to_utf8(cps, ls, le, title, outsz);
*num = numv;
return 1;
}
/* F:卷/部/集 + 空白 + 中文数字(卷一 / 卷 一 / 集三) */
if (is_volmark(cps[ls])) {
int i = ls + 1;
while (i < le && (cps[i] == ' ' || cps[i] == 0x3000)) i++;
int cstart = i;
while (i < le && is_cn_num_char(cps[i])) i++;
if (i == cstart) return 0;
int numv = cn_num_from_cps(cps, cstart, i);
if (numv < 0) return 0;
if (sent_punct_count(cps, i, le) > 1) return 0;
cps_to_utf8(cps, ls, le, title, outsz);
*num = numv;
return 1;
}
/* E:Chapter N(N=阿拉伯 / 罗马数字 / 英文数字词 one..hundred) */
if (cps_prefix_ci(cps, ls, le, "chapter")) {
int i = ls + 7;
if (i >= le) return 0;
if (!(cps[i] == ' ' || cps[i] == 0x3000 || cps[i] == '\t')) return 0; /* 必须空白:ChapterX 不认 */
while (i < le && (cps[i] == ' ' || cps[i] == 0x3000 || cps[i] == '\t')) i++;
int numv;
if (ascii_digit_val(cps[i]) >= 0) {
numv = 0; int cnt = 0;
while (i < le && ascii_digit_val(cps[i]) >= 0) { numv = numv * 10 + ascii_digit_val(cps[i]); i++; cnt++; }
if (cnt < 1 || cnt > 5) return 0;
} else {
int endi = i;
numv = roman_to_int(cps, i, le, &endi);
if (numv < 0) { endi = i; numv = word_num_from_cps(cps, i, le, &endi); }
if (numv < 0) return 0;
i = endi;
}
if (sent_punct_count(cps, i, le) > 1) return 0;
cps_to_utf8(cps, ls, le, title, outsz);
*num = numv;
return 1;
}
/* D:特殊前缀(楔子/序章/…)——要求整行无正文标点(避免「正文内容。」/「番外正文。」误判) */
for (int t = 0; t < G_SPECIAL_N; t++) {
if (cps_prefix(cps, ls, le, g_special[t])) {
int i = ls;
for (const uint8_t *p = g_special[t]; *p; ) { /* 消费前缀码点 */
uint8_t b0 = *p;
int n = (b0 < 0x80) ? 1 : ((b0 & 0xE0) == 0xC0) ? 2 : ((b0 & 0xF0) == 0xE0) ? 3 : 4;
i++; p += n;
}
if (sent_punct_count(cps, i, le) > 0) return 0;
cps_to_utf8(cps, ls, le, title, outsz);
*num = -1;
return 1;
}
}
return 0;
}
int rdchapter_scan(const char *seg_dir, rd_enc_t enc, const char *book_basename, rdchapter *c)
{
if (!c) return -1;
rdchapter_init(c);
if (!seg_dir || !seg_dir[0]) return 0; /* 无段目录 → 无章节 */
int nsegs = rdseg_n(seg_dir);
if (nsegs < 0) return -1;
if (nsegs == 0) { c->nsegs = 0; return 0; }
c->seg_char_start = (int64_t *)malloc((size_t)(nsegs + 1) * sizeof(int64_t));
if (!c->seg_char_start) return -6;
c->nsegs = nsegs;
int ent_cap = 0;
int64_t global = 0;
int err = 0;
uint8_t *raw = NULL; size_t rawcap = 0;
uint8_t *utf8 = NULL; size_t utf8cap = 0;
uint32_t *cps = NULL;
for (int seg = 0; seg < nsegs && !err; seg++) {
size_t rawlen = 0;
if (rdseg_rawlen(seg_dir, seg, &rawlen) != 0) { err = 1; break; }
if (rawlen > rawcap) {
uint8_t *t = (uint8_t *)realloc(raw, rawlen ? rawlen : 1);
if (!t) { err = -6; break; }
raw = t; rawcap = rawlen;
}
size_t rn = 0;
if (rdseg_load(seg_dir, seg, raw, rawlen, &rn) != 0) { err = 2; break; }
size_t ulen = 0;
size_t need = rawlen * 3 / 2 + 16;
if (need < RDTXT_SEG_UTF8_MAX + 8) need = RDTXT_SEG_UTF8_MAX + 8;
if (need > utf8cap) {
uint8_t *t = (uint8_t *)realloc(utf8, need);
if (!t) { err = -6; break; }
utf8 = t; utf8cap = need;
}
if (rd_to_utf8(enc, raw, rn, utf8, utf8cap, &ulen) != RD_ENC_OK) { err = 3; break; }
size_t n = 0;
/* 解码为码点失败(多为内存不足)→ 视作扫描失败:不落盘坏结果(见 rdchapter_scan 头部注释)。 */
if (rd_utf8_to_cp(utf8, ulen, &cps, &n) != RD_ENC_OK) { cps = NULL; err = -6; break; }
c->seg_char_start[seg] = global; /* 本段全局字符起点 */
if (cps && n > 0) {
int ls = 0;
while (ls < (int)n) {
int le = ls;
while (le < (int)n && cps[le] != '\n') le++;
int indent = 0, first = ls;
while (first < le && (cps[first] == ' ' || cps[first] == '\t' || cps[first] == 0x3000)) {
indent++; first++;
}
int te = le;
while (te > first && (cps[te - 1] == ' ' || cps[te - 1] == '\t' ||
cps[te - 1] == '\r' || cps[te - 1] == 0x3000)) te--;
if (indent <= RDCHAP_INDENT_MAX && first < te) {
char tbuf[RDCHAP_TITLE_MAX];
int num = -1;
if (chap_match_line(cps, first, te, tbuf, sizeof tbuf, &num) && c->n < RDCHAP_MAX_ENTRIES) {
if (ent_cap <= c->n) {
int nc = ent_cap ? ent_cap * 2 : 64;
if (nc > RDCHAP_MAX_ENTRIES) nc = RDCHAP_MAX_ENTRIES;
rdchapter_entry *ne = (rdchapter_entry *)realloc(c->entries, (size_t)nc * sizeof(rdchapter_entry));
if (!ne) { err = -6; break; }
c->entries = ne; ent_cap = nc;
}
rdchapter_entry *en = &c->entries[c->n];
memset(en, 0, sizeof *en);
snprintf(en->title, sizeof en->title, "%s", tbuf);
en->char_off = global + first;
en->seg_idx = seg;
en->seg_char = (int32_t)first;
en->number = num;
c->n++;
}
}
if (le >= (int)n) break;
ls = le + 1;
}
free(cps); cps = NULL;
}
global += (int64_t)n;
}
free(cps);
free(raw);
free(utf8);
if (err) { rdchapter_free(c); return err; }
/* 段数恒定:seg_char_start[nsegs] = 全书字符数(尾哨兵)。 */
if (global > (int64_t)0x7fffffffLL) { rdchapter_free(c); return -7; } /* 安全上限 */
c->seg_char_start[nsegs] = global;
/* 卷首/前置:第一章节前有内容 → 作为 entries[0](title=文件名或"卷首",号=-1)。 */
if (c->n > 0 && c->entries[0].char_off > 0) {
if (c->n >= RDCHAP_MAX_ENTRIES) { /* 已满则跳过卷首(理论) */ }
else {
memmove(&c->entries[1], &c->entries[0], (size_t)c->n * sizeof(rdchapter_entry));
rdchapter_entry *pre = &c->entries[0];
memset(pre, 0, sizeof *pre);
const char *bn = (book_basename && book_basename[0]) ? book_basename : "卷首";
strncpy(pre->title, bn, sizeof pre->title - 1);
pre->title[sizeof pre->title - 1] = 0;
pre->char_off = 0;
pre->seg_idx = 0;
pre->seg_char = 0;
pre->number = -1;
c->n++;
c->has_pre = 1;
snprintf(c->pre_title, sizeof c->pre_title, "%s", bn);
}
} else if (book_basename && book_basename[0]) {
snprintf(c->pre_title, sizeof c->pre_title, "%s", book_basename);
} else {
snprintf(c->pre_title, sizeof c->pre_title, "%s", "卷首");
}
return 0;
}
void rdchapter_init(rdchapter *c)
{
if (!c) return;
memset(c, 0, sizeof *c);
}
void rdchapter_free(rdchapter *c)
{
if (!c) return;
free(c->entries); c->entries = NULL;
free(c->seg_char_start); c->seg_char_start = NULL;
c->n = c->nsegs = c->has_pre = 0;
c->pre_title[0] = 0;
}
int rdchapter_cn_num(const char *s, int len)
{
if (!s || len <= 0) return -1;
uint32_t local[16];
int n = 0;
int i = 0;
while (i < len && n < 16) {
uint8_t b0 = (uint8_t)s[i];
uint32_t cp; int nb;
if (b0 < 0x80) { cp = b0; nb = 1; }
else if ((b0 & 0xE0) == 0xC0) { cp = ((uint32_t)(b0 & 0x1F) << 6) | ((uint8_t)s[i + 1] & 0x3F); nb = 2; }
else if ((b0 & 0xF0) == 0xE0) { cp = ((uint32_t)(b0 & 0x0F) << 12) | ((uint32_t)((uint8_t)s[i + 1] & 0x3F) << 6) | ((uint8_t)s[i + 2] & 0x3F); nb = 3; }
else { cp = ((uint32_t)(b0 & 0x07) << 18) | ((uint32_t)((uint8_t)s[i + 1] & 0x3F) << 12) | ((uint32_t)((uint8_t)s[i + 2] & 0x3F) << 6) | ((uint8_t)s[i + 3] & 0x3F); nb = 4; }
if (!is_cn_num_char(cp)) break;
local[n++] = cp;
i += nb;
}
if (n == 0) return -1;
return cn_num_from_cps(local, 0, n);
}
+61
View File
@@ -0,0 +1,61 @@
/* core/rdchapter.h — TXT 章节规范解析器:扫描已切好的 512KB 码位对齐段文件(core/seg.c),
* 识别章节标题行,输出章节列表(每项:标题、全书字符偏移、所在段/段内字符偏移、阿拉伯章号)。
*
* 设计约束:
* - 逐段流式(`rdseg_load` 读一个段 → `rd_to_utf8` 解码 → `rd_utf8_to_cp` 数码点 → 行扫),
* 不整书驻留;单段内存与 book.c `seg_ensure_built` 同量级(~512KB 原文 + ~3 倍 UTF-8)。
* - 章节标题行约束:行首缩进 ≤4 半角/全角空格;标题显示宽度 ≤ RDCHAP_DISP_WMAX(CJK=2/ASCII=1);
* 行内正文标点 ≤1(句号/逗号/省略号等,容「极少」);行首锚定 + 短 + 无成段标点 → 防误判
* (「他说第三章内容」不居行首即不命中;目录页交错等已知限制,见报告)。
* - 中文数字 → 阿拉伯(`rdchapter_cn_num`,用于排序/显示);按 char_off 升序输出。
* - 开头书名/简介区(第一章节之前的内容)归为「卷首/前置」,作为条目[0](title=文件名或"卷首")。
*
* 纯 libc + rdenc + seg,无 SDK/FreeType;宿主可测。
*/
#ifndef RDCHAPTER_H
#define RDCHAPTER_H
#include "rdenc.h"
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define RDCHAP_TITLE_MAX 160 /* 单个标题字符(UTF-8 字节)上限 */
#define RDCHAP_DISP_WMAX 40 /* 标题最大显示宽度(CJK=2 / ASCII=1) */
#define RDCHAP_INDENT_MAX 4 /* 行首缩进上限(空格/全角空格/制表,按字符计) */
#define RDCHAP_MAX_ENTRIES 200000 /* 章节数上限(防失控内存) */
typedef struct rdchapter_entry {
char title[RDCHAP_TITLE_MAX]; /* 完整标题行(如「第一章 风雨」「楔子」),UTF-8 */
int64_t char_off; /* 全书字符偏移(段 0 起,跨段累计;标题首字符) */
int32_t seg_idx; /* 所在 512KB 段下标 */
int32_t seg_char; /* 段内字符偏移(标题首字符) */
int32_t number; /* 阿拉伯章号;-1=无号(如楔子/卷首/序章) */
} rdchapter_entry;
typedef struct rdchapter {
rdchapter_entry *entries;
int n; /* 章节数;0=本书无章节(扫描过) */
int64_t *seg_char_start;/* seg_char_start[seg]=该段在全书中的字符起点;[nsegs]=全书字符数 */
int nsegs; /* 段数(= seg_char_start 数组长 -1) */
char pre_title[RDCHAP_TITLE_MAX]; /* 卷首/前置标题(文件名或"卷首") */
int has_pre; /* 1=存在卷首/前置(第一章节前有内容) */
} rdchapter;
void rdchapter_init(rdchapter *c);
void rdchapter_free(rdchapter *c);
/* 扫描 seg_dir 下全部 512KB 段文件,识别章节标题。book_basename 用于卷首标题(可为 NULL/"卷首")。
* 0=成功(c->n=章节数,可为 0);负=错误(seg_dir 无效/无段/解码失败等)。 */
int rdchapter_scan(const char *seg_dir, rd_enc_t enc, const char *book_basename, rdchapter *c);
/* 中文数字 → 阿拉伯(「二十三」→23,「一百零一」→101,「两」→2)。s/len 为 UTF-8;非法返回 -1。 */
int rdchapter_cn_num(const char *s, int len);
#ifdef __cplusplus
}
#endif
#endif /* RDCHAPTER_H */
+6 -1
View File
@@ -312,7 +312,12 @@ int rd_utf8_to_cp(const uint8_t *utf8, size_t len, uint32_t **cps, size_t *n)
size_t cnt = 0, i = 0;
while (i < len) {
uint8_t b = utf8[i];
if (b < 0x80) { arr[cnt++] = b; i++; continue; }
if (b < 0x80) {
/* \r\n → \n、孤 \r → \n:TXT 行尾 \r 若进排版,在无 U+000D 映射的字体里落成
* .notdef(豆腐/占宽);归一化后章节/页表/进度锚同处一个无 \r 码点空间。 */
if (b == '\r') { arr[cnt++] = '\n'; i++; if (i < len && utf8[i] == '\n') i++; continue; }
arr[cnt++] = b; i++; continue;
}
unsigned q, lo, hi;
if ((b & 0xE0) == 0xC0) { q = 2; lo = 0x80; hi = 0xBF; }
else if ((b & 0xF0) == 0xE0) { q = 3; lo = 0x80; hi = 0xBF; }
+63
View File
@@ -226,6 +226,12 @@ int rd_layout(const rd_layout_in *in, rd_layout_out *out)
npages++;
page_start = np_start;
cur_y = 0;
/* 孤行上推修正:被推行(l-1)成为新页首行 → 按页首行重算其 y 并计入页内高度,
* 否则它带着上一页的 y 残值(渲染错位)且页填充少算一行高(页底溢出)。 */
if (np_start == l - 1) {
out->lines[l - 1].y = 0;
cur_y = lh;
}
}
/* 重新计算本行在本页的 top(若 page_start 因孤儿调整而变化,extra 需重算) */
extra = (out->lines[l].para_start && l > page_start) ? pspacing : 0;
@@ -245,6 +251,63 @@ int rd_layout(const rd_layout_in *in, rd_layout_out *out)
return RD_LAYOUT_OK;
}
int rd_layout_page(const rd_layout_in *in, int32_t start, int mid_para,
rd_line *lines, int max_lines, int *nlines, int32_t *next_start)
{
if (!in || !in->chars || in->n < 0 || !lines || !nlines || !next_start || max_lines <= 0)
return RD_LAYOUT_ERR_INVAL;
if (in->fs <= 0 || in->page_w <= 0 || in->page_h <= 0) return RD_LAYOUT_ERR_INVAL;
if (start < 0) start = 0;
if (start > in->n) start = in->n;
int lh = (in->fs * in->line_scale) / 100;
if (lh < 1) lh = 1;
int pspacing = lh / 2;
if (pspacing < 0) pspacing = 0;
int nl = 0;
int y = 0; /* 页内累计(含段距;规则同 rd_layout 阶段 B 单页填充) */
int32_t ps = start;
int first_para = 1; /* 首段:首行状态由 mid_para 给定(续行不缩进/非段首) */
while (ps < in->n) {
while (ps < in->n && (in->chars[ps] == '\n' || in->chars[ps] == '\r')) ps++;
if (ps >= in->n) break;
int32_t pe = ps;
while (pe < in->n && in->chars[pe] != '\n') pe++;
int32_t pos = ps;
int first_line = first_para ? !mid_para : 1;
first_para = 0;
while (pos < pe) {
rd_line ln;
para_break_line(in, pe, &pos, first_line, &ln);
first_line = 0;
/* 页填充判定(同 rd_layout 阶段 B:页内首行不加段距;溢出即页满) */
int extra = (ln.para_start && nl > 0) ? pspacing : 0;
if (y + lh + extra > in->page_h && nl > 0) {
/* 孤行守卫:页末行是段首行且段未结束 → 该行随段入下一页(同 rd_layout) */
int cut = nl;
if (lines[nl - 1].para_start && !lines[nl - 1].para_end) cut = nl - 1;
*nlines = cut;
*next_start = (cut < nl) ? lines[cut].s : ln.s;
return RD_LAYOUT_OK;
}
if (nl >= max_lines) { *nlines = nl; *next_start = ln.s; return RD_LAYOUT_ERR_OVER; }
ln.y = y + extra;
y += lh + extra;
lines[nl++] = ln;
if (ln.para_end) break;
}
ps = pos;
while (ps < in->n && in->chars[ps] == '\n') ps++;
if (pos >= pe && ps >= in->n) break;
}
*nlines = nl;
*next_start = in->n;
return RD_LAYOUT_OK;
}
int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char)
{
if (!out || out->npages <= 0) return 0;
+8
View File
@@ -65,6 +65,14 @@ typedef struct rd_layout_out {
int rd_layout(const rd_layout_in *in, rd_layout_out *out);
/* 页级微排版(Phase 2C):从 chars[start](须为行首:start==0 或 chars[start-1]=='\n')起只排一页,
* 填满 page_h 即停。mid_para=1 表示 start 行是段中续行(不缩进、非段首行);=0 表示段首行
* (缩进 + para_start,与 rd_layout 首行规则一致)。输出 lines[0..*nlines)(内容区坐标,
* 与 rd_layout 同断行/分页/孤行守卫规则);*next_start = 下一页首字符偏移(== in->n 表示文末)。
* 一致性:同一 (chars, start, mid_para) 下产出 == rd_layout 全排版中该页的行区间(逐行 diff 可证)。 */
int rd_layout_page(const rd_layout_in *in, int32_t start, int mid_para,
rd_line *lines, int max_lines, int *nlines, int32_t *next_start);
/* 重排锚定:在 layout 输出里找第一个满足 `line.s <= anchor_char < 下个 line.s` 的**行**所属的页。
* 返回页下标(0..npages-1);anchor_char 越界 clamp 到首/末页。 */
int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char);
+6 -4
View File
@@ -1,10 +1,11 @@
#!/bin/bash
# pack_reader_mpk.sh — 把 os.meow.reader 打成 .mpk(tar.gz),并把 LGPL 合规材料并入包内。
# .mpk = app.json + reader 二进制 + icon.png + resources/licenses/{NOTICE,GPL-3.0.txt,LGPL-3.0.txt}
# .mpk = app.json + reader 二进制 + icon.png + resources/licenses/{NOTICE,GPL-3.0.txt,LGPL-3.0.txt,FTL.txt}
# (不含 data/cache;符合 APPS.md §3 与 mips-repo/mpk/README.md 的 .mpk 规范)
#
# 合规目的:让「接收者随包拿到」NOTICE + GPL-3.0 全文 + LGPL-3.0 全文,
# 满足 LGPL-3.0 §4(a)(显著通知)与 §4(b)(随 Combined Work 附 GPL + LGPL 两份文本)。
# 合规目的:让「接收者随包拿到」NOTICE + GPL-3.0 全文 + LGPL-3.0 全文 + FreeType(FTL) 全文,
# 满足 LGPL-3.0 §4(a)(显著通知)、§4(b)(随 Combined Work 附 GPL + LGPL 两份文本)与
# FTL 署名条款(随二进制分发附许可文本与版权声明)。
#
# 用法:
# ./build_reader.sh # 先编出 reader 二进制(本脚本不重编)
@@ -31,6 +32,7 @@ cp icon.png "$STAGE/" 2>/dev/null || true
cp reader "$STAGE/"
mkdir -p "$STAGE/resources/licenses"
cp resources/licenses/NOTICE resources/licenses/GPL-3.0.txt resources/licenses/LGPL-3.0.txt \
resources/licenses/FTL.txt \
"$STAGE/resources/licenses/"
# busybox tar + gzip(与 launcher/nyai 同管道语义);-cz 直接出 .
@@ -40,6 +42,6 @@ echo "== packed $MPK =="
tar tzvf "$MPK"
echo "md5: $(md5sum "$MPK" | cut -d' ' -f1)"
echo
echo "含合规材料:resources/licenses/NOTICE + GPL-3.0.txt + LGPL-3.0.txt"
echo "含合规材料:resources/licenses/NOTICE + GPL-3.0.txt + LGPL-3.0.txt + FTL.txt"
echo "未签名。正式分发前 run:"
echo " ../meow_sign/meow_sign sign $MPK \"\$HOME/Project/R3ProII/.keys/signing.key\""
+5
View File
@@ -34,6 +34,11 @@ void reader_on_touch_move(int x, int y);
* 返回 0=成功(书已打开、页位图已备、可 push);非 0=失败(未能打开,调用方不 push)。 */
int reader_open(const char *path);
/* R3.4 章节重解析(app/settings.c 调用):是否打开了支持章节解析的书(TXT+seg 模式)。
* reader_reparse_chapters 返回 n=章节数 / 0=无章节(成功)/ -1=不可用 / -2=扫描失败(未落盘)。 */
int reader_has_chapter_book(void);
int reader_reparse_chapters(void);
/* ---- 全局设置页(app/settings.c,C)---- */
meow_view *settings_view_create(void);
int settings_is_active(void);
+105 -1
View File
@@ -20,10 +20,82 @@ static void write_txt(const char *path)
"它包含汉字、标点,以及一些 English words 混合排版。"
"断行、避头尾与分页行为都要正确。\n", i + 1);
}
fprintf(f, "\nChapter two: the quick brown fox jumps over the lazy dog.\n");
fprintf(f, "\n第二章 尾声\n这是一段章尾正文,用于验证章节跳转与章内翻页。\n");
fclose(f);
}
/* ---- Phase 2A/2C:跨段拼接页连续性(硬用例)。
* 构造恰好 512KB 的段 0(5242×100B + 88B,末行为短段落),段 1 开头放唯一标记 SEG1MARKER;
* 建段 1 触发滚动拼接后,段界页(段 0 末页)应包含段 1 开头的文本(拼接页含两段文本),
* 且跨段往返翻页后锚 (seg,ch) 稳定。 ---- */
typedef struct { char *buf; size_t len, cap; } linecol;
static void linecol_add(int x, int y, const char *utf8, int len, void *ctx)
{
(void)x; (void)y;
linecol *lc = (linecol *)ctx;
if (lc->len + (size_t)len + 2 > lc->cap) {
size_t nc = lc->cap ? lc->cap * 2 : 4096;
while (nc < lc->len + (size_t)len + 2) nc *= 2;
char *nb = (char *)realloc(lc->buf, nc);
if (!nb) return;
lc->buf = nb; lc->cap = nc;
}
memcpy(lc->buf + lc->len, utf8, (size_t)len);
lc->len += (size_t)len;
lc->buf[lc->len++] = '\n';
lc->buf[lc->len] = 0;
}
static void phase2a_stitch(const char *font)
{
const char *txt = "/tmp/rdbook_stitch.txt";
FILE *f = fopen(txt, "wb");
char line[100];
memset(line, 'a', 99); line[99] = '\n';
for (int i = 0; i < 5242; i++) fwrite(line, 1, 100, f); /* 524200 B */
for (int i = 0; i < 88; i++) fputc('a', f); /* 共 524288 B = 512KB 码位边界 */
fprintf(f, "SEG1MARKER 段一开头唯一标记,证明拼接页跨段包含两段文本。\n");
for (int i = 0; i < 2000; i++)
fprintf(f, "段一正文第%d行:滚动拼接后段界无短页,页表与进度锚稳定。\n", i + 1);
fclose(f);
rd_book_set_seg_base("/tmp/meow_seg_stitch");
rd_book_set_index_dir("/tmp/meow_seg_stitch_index");
rd_book *b = NULL;
int rc = rd_book_open(&b, txt, font, 20, 130, 480, 720, 16, 20, 62);
CHECK(rc == 0, "stitch open");
if (rc != 0) { remove(txt); return; }
int nseg = rd_book_seg_n(b);
CHECK(nseg >= 2, "stitch multi-seg");
if (nseg >= 2) {
int p1 = rd_book_seg_page(b, 1); /* 建段 1 → 触发段 0 末页拼接 */
CHECK(p1 >= 0, "stitch seg1 built");
int ps = rd_book_seg_page(b, 0);
int pc = rd_book_seg_page_count(b, 0);
CHECK(ps == 0 && pc > 0, "stitch seg0 pages");
int bnd = ps + pc - 1;
linecol lc = { NULL, 0, 0 };
int rr = rd_book_render_text(b, bnd, linecol_add, &lc);
CHECK(rr == 0, "stitch boundary render_text ok");
CHECK(lc.buf && strstr(lc.buf, "SEG1MARKER") != NULL, "stitch boundary contains seg1 text");
free(lc.buf);
/* 跨段往返:段界页翻前/后,锚 (seg,ch) 不变 */
int as = -1; int32_t ach = -1;
rd_book_set_page(b, bnd);
rd_book_anchor(b, &as, &ach);
CHECK(as == 0 && ach >= 0, "stitch boundary anchor seg0");
int cur = rd_book_current(b);
int fwd = rd_book_turn(b, 1);
int back = rd_book_turn(b, -1);
CHECK(fwd == cur + 1 && back == cur, "stitch boundary turn roundtrip");
int as2 = -1; int32_t ach2 = -1;
rd_book_anchor(b, &as2, &ach2);
CHECK(as2 == as && ach2 == ach, "stitch boundary anchor stable");
}
rd_book_close(b);
remove(txt);
}
int main(int argc, char **argv)
{
const char *font = (argc > 1) ? argv[1] : "/home/Aska/Project/R3ProII/font/src/MiSans-Regular.otf";
@@ -82,8 +154,40 @@ int main(int argc, char **argv)
int np3 = rd_book_set_size(b, 20, 150);
CHECK(np3 >= 0 && np3 < rd_book_pages(b), "set_size reflow ok");
/* R3.3 章节:解析 + 章节→页映射 + 章内翻页单调(页号递增不跳变) */
int nch = rd_book_chapter_n(b);
printf(" chapters=%d\n", nch);
CHECK(nch == 2, "book has 2 chapters");
if (nch == 2) {
char t0[160], t1[160]; int n0 = -1, n1 = -1; int64_t c0 = -1, c1 = -1;
CHECK(rd_book_chapter(b, 0, t0, sizeof t0, &c0, NULL, NULL, &n0) == 0, "chapter0 get");
CHECK(n0 == 1, "chapter0 number 1");
CHECK(c0 == 0, "chapter0 char_off 0");
int p0 = rd_book_page_for_char(b, c0);
CHECK(p0 == 0 && p0 < rd_book_pages(b), "chapter0 start page 0");
CHECK(rd_book_chapter(b, 1, t1, sizeof t1, &c1, NULL, NULL, &n1) == 0, "chapter1 get");
CHECK(n1 == 2, "chapter1 number 2");
int p1 = rd_book_page_for_char(b, c1);
CHECK(p1 >= p0 && p1 < rd_book_pages(b), "chapter1 start page in range");
printf(" chapter1('%s') at page %d (of %d)\n", t1, p1, rd_book_pages(b));
/* 跳到章二首页,验证章内翻页 +1 单调(不越过章界仍递增) */
int landed = rd_book_set_page(b, p1);
CHECK(landed == p1, "jump lands on chapter1 start");
int cur1 = rd_book_current(b);
int nxt1 = rd_book_turn(b, 1);
CHECK((nxt1 == cur1 + 1) || (nxt1 == cur1), "chapter-internal turn +1 (clamp-safe)");
/* R3.4 手动重解析(清 .chap 缓存 + 重扫)应幂等返回同章数,且章节仍可用 */
int nrep = rd_book_reparse_chapters(b);
CHECK(nrep == nch, "reparse returns same chapter count");
CHECK(rd_book_chapter_n(b) == nch, "reparse keeps chapters");
}
free(buf);
rd_book_close(b);
/* Phase 2A/2C:跨段拼接页连续性 + 段界锚稳定(另开一本 >512KB 的多段书) */
phase2a_stitch(font);
printf("book_smoke: %s\n", fails ? "FAIL" : "PASS");
return fails ? 1 : 0;
}
@@ -0,0 +1,20 @@
#!/bin/bash
# test/build_turn_repro.sh — cross-compile turn_repro for mipsel (mirrors book_smoke_mipsel link line).
set -e
cd "$(dirname "$0")/.."
ROOT="$HOME/Project/R3ProII"
CC="$ROOT/.tools/zig-mipsel-cc"
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
VENDOR="$PWD/vendor"
OBJ="$VENDOR/build"
FT_BUILD="$VENDOR/freetype-build"
FT_SRC="$VENDOR/freetype-src"
FT_INCS="-I$FT_BUILD -I$FT_BUILD/include -I$FT_SRC/include"
COREINC="-Icore -Ivendor/miniz -Ivendor/stb -Ivendor/libmobi/src -Ivendor/zlib"
$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC \
test/turn_repro.c \
app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/seg.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \
core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c engine/rdft.c \
"$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" \
-o test/turn_repro_mipsel
echo "build OK -> test/turn_repro_mipsel"
+275
View File
@@ -0,0 +1,275 @@
/* test/chapter_smoke.c — os.meow.reader 章节规范解析器宿主单测(纯 C,无 SDK/Freetype)。
*
* 覆盖(按需求 5 样本 + 跨段 + 中文数字转换):
* C1 标准中文数字章(第一章/第二章/第十一章 → 1/2/11,char_off 单调)
* C2 阿拉伯章(第1章/第2章/第3章 → 1/2/3)
* C3 楔子/序章/尾声/番外(无号,number==-1,char_off 单调)
* C4 无章节纯文本(n==0,seg_char_start 仍完整)
* C5 开头长书名+简介再进第一章(卷首识别 as entries[0]、章号、防误判正文行 )
* C6 跨段:>512KB 书,章落在后段(seg_idx>=1,char_off 跨段单调)
* C7 中文数字转换:十一/二十三/一百零一/一千零一/两
*
* 跑法:build_reader.sh 内 `test/chapter_smoke_host`;本文件只依赖 rdchapter + seg + rdenc。
*/
#include "rdchapter.h"
#include "seg.h"
#include "rdenc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int g_pass = 0, g_fail = 0;
#define CHECK(cond, msg) do { if (cond) { g_pass++; } else { g_fail++; printf("FAIL %s:%d %s\n", __FILE__, __LINE__, msg); } } while (0)
static char g_dir[600];
static void write_file(const char *path, const char *data)
{
FILE *f = fopen(path, "wb");
fwrite(data, 1, strlen(data), f);
fclose(f);
}
/* 建段目录 + 切段,返回段数(<0=错误)。 */
static int seg_for(const char *path, const char *base)
{
rdseg_set_base(base);
char dir[600];
if (rdseg_dir_for(path, dir, sizeof dir) != 0) return -1;
strcpy(g_dir, dir);
return rdseg_build(path, RD_ENC_UTF8, dir);
}
static int scan_book(rdchapter *c, const char *basename)
{
return rdchapter_scan(g_dir, RD_ENC_UTF8, basename, c);
}
static void test_c1(void)
{
const char *txt = "/tmp/ch_s1.txt";
write_file(txt,
"第一章 风起\n"
"这是第一章的正文内容。\n"
"第二章 云涌\n"
"这是第二章的正文内容。\n"
"第十一章 破晓\n"
"这是第十一章的正文内容。\n");
CHECK(seg_for(txt, "/tmp/ch_base1") >= 1, "c1 seg build");
rdchapter c; rdchapter_init(&c);
CHECK(scan_book(&c, "s1") == 0, "c1 scan");
CHECK(c.n == 3, "c1 n==3");
if (c.n == 3) {
CHECK(c.entries[0].number == 1, "c1 #1");
CHECK(c.entries[1].number == 2, "c1 #2");
CHECK(c.entries[2].number == 11, "c1 #11");
CHECK(c.entries[0].char_off == 0, "c1 first at 0 (no preface)");
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
c.entries[1].char_off < c.entries[2].char_off, "c1 char_off monotonic");
CHECK(c.entries[0].seg_idx == 0 && c.entries[0].seg_char == 0, "c1 seg0 char0");
CHECK(strcmp(c.entries[0].title, "第一章 风起") == 0, "c1 title");
CHECK(c.has_pre == 0, "c1 no preface");
}
rdchapter_free(&c);
remove(txt);
}
static void test_c2(void)
{
const char *txt = "/tmp/ch_s2.txt";
write_file(txt,
"第1章 起源\n"
"正文A。\n"
"第2章 发展\n"
"正文B。\n"
"第3章 终局\n"
"正文C。\n");
CHECK(seg_for(txt, "/tmp/ch_base2") >= 1, "c2 seg build");
rdchapter c; rdchapter_init(&c);
CHECK(scan_book(&c, "s2") == 0, "c2 scan");
CHECK(c.n == 3, "c2 n==3");
if (c.n == 3) {
CHECK(c.entries[0].number == 1, "c2 #1");
CHECK(c.entries[1].number == 2, "c2 #2");
CHECK(c.entries[2].number == 3, "c2 #3");
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
c.entries[1].char_off < c.entries[2].char_off, "c2 char_off monotonic");
}
rdchapter_free(&c);
remove(txt);
}
static void test_c3(void)
{
const char *txt = "/tmp/ch_s3.txt";
write_file(txt,
"楔子\n"
"一段开篇。\n"
"第一章 正传\n"
"正文内容。\n"
"尾声\n"
"后记文字。\n"
"番外 归途\n"
"番外正文。\n");
CHECK(seg_for(txt, "/tmp/ch_base3") >= 1, "c3 seg build");
rdchapter c; rdchapter_init(&c);
CHECK(scan_book(&c, "s3") == 0, "c3 scan");
CHECK(c.n == 4, "c3 n==4");
if (c.n == 4) {
CHECK(c.entries[0].number == -1, "c3 楔子 number -1");
CHECK(strcmp(c.entries[0].title, "楔子") == 0, "c3 楔子 title");
CHECK(c.entries[1].number == 1, "c3 第一章 number 1");
CHECK(c.entries[2].number == -1, "c3 尾声 number -1");
CHECK(c.entries[3].number == -1, "c3 番外 number -1");
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
c.entries[1].char_off < c.entries[2].char_off &&
c.entries[2].char_off < c.entries[3].char_off, "c3 char_off monotonic");
}
rdchapter_free(&c);
remove(txt);
}
static void test_c4(void)
{
const char *txt = "/tmp/ch_s4.txt";
write_file(txt,
"这是一个没有章节标题的长篇小说文本。\n"
"它只有连续不断的段落,没有任何章回节的标识。\n"
"所有的文字都是纯粹的故事内容。\n");
CHECK(seg_for(txt, "/tmp/ch_base4") >= 1, "c4 seg build");
rdchapter c; rdchapter_init(&c);
CHECK(scan_book(&c, "s4") == 0, "c4 scan");
CHECK(c.n == 0, "c4 no chapters");
CHECK(c.nsegs == 1, "c4 nsegs==1");
CHECK(c.seg_char_start && c.seg_char_start[0] == 0 && c.seg_char_start[1] > 0, "c4 seg_char_start");
rdchapter_free(&c);
remove(txt);
}
static void test_c5(void)
{
const char *txt = "/tmp/ch_s5.txt";
write_file(txt,
"《大漠孤烟》这是一本很长的书名,作者著。\n"
"简介:这是一个故事的开头,讲述少年成长。\n"
"第一章风雨欲来,全书开场,命运由此转折。\n"
"第一章 离家\n"
"正文内容。\n"
"第二章 远行\n"
"正文内容。\n");
CHECK(seg_for(txt, "/tmp/ch_base5") >= 1, "c5 seg build");
rdchapter c; rdchapter_init(&c);
CHECK(scan_book(&c, "sample5") == 0, "c5 scan");
CHECK(c.n == 3, "c5 n==3 (preface + 2 chapters)");
if (c.n == 3) {
CHECK(c.has_pre == 1, "c5 has preface");
CHECK(c.entries[0].number == -1, "c5 preface number -1");
CHECK(strcmp(c.entries[0].title, "sample5") == 0, "c5 preface title = basename");
CHECK(c.entries[0].char_off == 0, "c5 preface char_off 0");
CHECK(c.entries[1].number == 1 && c.entries[2].number == 2, "c5 chapter numbers 1/2");
CHECK(c.entries[1].char_off > 0, "c5 ch1 after preface");
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
c.entries[1].char_off < c.entries[2].char_off, "c5 monotonic");
/* 防误判:正文行「第一章风雨欲来,全书开场。」带两个正文标点 → 未被识别为章 */
CHECK(c.entries[1].seg_char > 0, "c5 decoy line not counted (ch1 seg_char>0)");
}
rdchapter_free(&c);
remove(txt);
}
static void test_c6(void)
{
/* 跨段:写 ~600KB filler,在 ~600KB 处放一个章 → 应落在后段(seg_idx>=1) */
const char *txt = "/tmp/ch_s6.txt";
FILE *f = fopen(txt, "wb");
const char *para = "这是一段用于填充页面的中文正文,包含标点,中文English混合,反复出现,占据空间。\n";
size_t plen = strlen(para);
size_t total = 0;
while (total < 600 * 1024) {
fwrite(para, 1, plen, f); total += plen;
}
fwrite("第二百章 跨段检测\n", 1, strlen("第二百章 跨段检测\n"), f);
const char *tail = "这是跨段之后的一小段正文。\n";
fwrite(tail, 1, strlen(tail), f);
fclose(f);
CHECK(seg_for(txt, "/tmp/ch_base6") >= 2, "c6 seg build >=2");
rdchapter c; rdchapter_init(&c);
CHECK(scan_book(&c, "s6") == 0, "c6 scan");
/* 前置 filler(600KB 无章节)→ entries[0]=卷首;真正章节在后段 */
CHECK(c.n == 2, "c6 n==2 (preface + 1 chapter)");
if (c.n == 2) {
CHECK(c.entries[0].number == -1, "c6 preface number -1");
CHECK(c.entries[1].number == 200, "c6 number 200");
CHECK(c.entries[1].seg_idx >= 1, "c6 chapter in later segment");
CHECK(c.entries[1].seg_idx < c.nsegs, "c6 seg_idx in range");
CHECK(c.entries[1].seg_char >= 0, "c6 seg_char >=0");
CHECK(c.seg_char_start[0] == 0 &&
c.seg_char_start[0] < c.seg_char_start[1] &&
c.seg_char_start[c.nsegs - 1] < c.seg_char_start[c.nsegs], "c6 seg_char_start monotonic");
/* 跨段 char_off:章首偏移应在段 1 起点之后 */
CHECK(c.entries[1].char_off >= c.seg_char_start[c.entries[1].seg_idx], "c6 char_off within seg");
}
rdchapter_free(&c);
remove(txt);
}
static void test_c7(void)
{
CHECK(rdchapter_cn_num("十一", 6) == 11, "c7 十一=11");
CHECK(rdchapter_cn_num("二十三", 9) == 23, "c7 二十三=23");
CHECK(rdchapter_cn_num("一百零一", 12) == 101, "c7 一百零一=101");
CHECK(rdchapter_cn_num("一千零一", 12) == 1001, "c7 一千零一=1001");
CHECK(rdchapter_cn_num("两", 3) == 2, "c7 两=2");
CHECK(rdchapter_cn_num("一百", 6) == 100, "c7 一百=100");
CHECK(rdchapter_cn_num("foobar", 6) == -1, "c7 invalid -1");
}
static void test_c8(void)
{
/* R3.4 英文章节:文字数字(one/twenty-one/two hundred fifty)+ 罗马(IV)+ 拒绝无空格(ChapterX) */
const char *txt = "/tmp/ch_s8.txt";
write_file(txt,
"Chapter one\n"
"正文。\n"
"Chapter IV\n"
"正文。\n"
"Chapter twenty-one\n"
"正文。\n"
"Chapter two hundred fifty\n"
"正文。\n"
"ChapterX\n"
"正文。\n"
"Chapter twelve\n"
"正文。\n");
CHECK(seg_for(txt, "/tmp/ch_base8") >= 1, "c8 seg build");
rdchapter c; rdchapter_init(&c);
CHECK(scan_book(&c, "s8") == 0, "c8 scan");
/* ChapterX 不认(无空格);其余 5 个认 */
CHECK(c.n == 5, "c8 n==5");
if (c.n == 5) {
static const int exp[5] = { 1, 4, 21, 250, 12 };
for (int i = 0; i < 5; i++) CHECK(c.entries[i].number == exp[i], "c8 word number value");
CHECK(c.entries[0].char_off < c.entries[1].char_off &&
c.entries[1].char_off < c.entries[2].char_off &&
c.entries[2].char_off < c.entries[3].char_off &&
c.entries[3].char_off < c.entries[4].char_off, "c8 char_off monotonic");
CHECK(strcmp(c.entries[0].title, "Chapter one") == 0, "c8 title Chapter one");
CHECK(strcmp(c.entries[3].title, "Chapter two hundred fifty") == 0, "c8 title two hundred fifty");
}
rdchapter_free(&c);
remove(txt);
}
int main(void)
{
test_c1();
test_c2();
test_c3();
test_c4();
test_c5();
test_c6();
test_c7();
test_c8();
printf("chapter_smoke: %d pass, %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+111
View File
@@ -341,10 +341,121 @@ static void t13(void)
CHECK(rd_enc_detect(gbk, sizeof gbk, NULL) == RD_ENC_GB18030, "t13 gbk still gb18030");
}
/* t14 — TXT `\r` 归一化(READER_ADVANCED §3-1):`\r\n`→`\n`、孤 `\r`→`\n`,
* 码点流不含 0x0D(行尾 \r 在无 U+000D 映射的字体里落成 .notdef 豆腐;MiSans 虽映射到
* 零宽空字形不可见,归一化后对所有字体稳健,且章节/页表/进度锚同处无 \r 码点空间)。 */
static void t14(void)
{
const uint8_t u8[] = "ab\r\ncd\ref\n\r\ngh\r";
uint32_t *cps = NULL; size_t n = 0;
CHECK(rd_utf8_to_cp(u8, sizeof u8 - 1, &cps, &n) == RD_ENC_OK, "t14 utf8→cp ok");
const uint32_t expect[] = { 'a','b','\n','c','d','\n','e','f','\n','\n','g','h','\n' };
CHECK(n == sizeof expect / sizeof expect[0], "t14 count");
int no_cr = 1;
for (size_t i = 0; i < n; i++) if (cps[i] == 0x0D) no_cr = 0;
CHECK(no_cr, "t14 no 0x0D in cps");
CHECK(n == 13 && memcmp(cps, expect, n * sizeof(uint32_t)) == 0, "t14 normalized sequence");
free(cps);
/* GB18030 路径同样归一化(rd_to_utf8 → rd_utf8_to_cp) */
const uint8_t gbk[] = { 0xC4,0xE3,0xBA,0xC3, 0x0D,0x0A, 0xCA,0xC0,0xBD,0xE7 }; /* 你好\r\n世界 */
uint8_t out[64]; size_t olen = 0;
CHECK(rd_to_utf8(RD_ENC_GB18030, gbk, sizeof gbk, out, sizeof out, &olen) == RD_ENC_OK, "t14 gbk decode");
uint32_t *c2 = NULL; size_t n2 = 0;
CHECK(rd_utf8_to_cp(out, olen, &c2, &n2) == RD_ENC_OK, "t14 gbk→cp");
CHECK(n2 == 5 && c2[0] == 0x4F60 && c2[2] == '\n' && c2[3] == 0x4E16, "t14 gbk crlf→lf");
free(c2);
}
/* t15 — 页级微排版 vs 全排版逐行一致(Phase 2C 硬用例):
* 全排版 rd_layout → 对每页 first_char 起 rd_layout_page,断言行区间(s/e/x/y/w/para 标志)
* 与该页完全一致,且 next_start == 下一页 first_char(或文末)。含伪随机长文覆盖段界/空段/
* Kinsoku/长词/页满边界;显式构造 mid_para=1(段中续行起始)。 */
static uint32_t g_lcg = 0x12345678u;
static uint32_t lcg(void) { g_lcg = g_lcg * 1664525u + 1013904223u; return g_lcg >> 8; }
static void t15_check_text(const char *txt, int fs, int ls, int pw, int ph, const char *tag)
{
uint32_t *cps = NULL; size_t n = 0;
rd_utf8_to_cp((const uint8_t *)txt, strlen(txt), &cps, &n);
rd_line *fl = (rd_line *)malloc((n + 1) * sizeof(rd_line));
rd_page *fp = (rd_page *)malloc((n + 1) * sizeof(rd_page));
rd_line *ml = (rd_line *)malloc((size_t)(ph + 4) * sizeof(rd_line));
CHECK(fl && fp && ml, "t15 alloc");
rd_layout_in in = { cps, (int32_t)n, fs, ls, pw, ph, fs, meas_mock, NULL };
rd_layout_out out;
out.lines = fl; out.pages = fp; out.max_lines = (int)n + 1; out.max_pages = (int)n + 1;
int rc = rd_layout(&in, &out);
CHECK(rc == RD_LAYOUT_OK, tag);
if (rc != RD_LAYOUT_OK) { free(cps); free(fl); free(fp); free(ml); return; }
for (int p = 0; p < out.npages; p++) {
int32_t start = out.pages[p].first_char;
int mid = !(start == 0 || cps[start - 1] == '\n');
int mnl = -1; int32_t nxt = -1;
int mrc = rd_layout_page(&in, start, mid, ml, ph + 3, &mnl, &nxt);
CHECK(mrc == RD_LAYOUT_OK, "t15 micro rc");
CHECK(mnl == out.pages[p].nlines, "t15 page nlines equal");
if (mnl > 0 && mnl == out.pages[p].nlines) {
for (int li = 0; li < mnl; li++) {
const rd_line *a = &out.lines[out.pages[p].first_line + li];
const rd_line *b = &ml[li];
CHECK(a->s == b->s && a->e == b->e && a->x == b->x && a->y == b->y &&
a->w == b->w && a->para_start == b->para_start && a->para_end == b->para_end,
"t15 line-for-line equal");
}
}
int32_t want_next = (p + 1 < out.npages) ? out.pages[p + 1].first_char : (int32_t)n;
CHECK(nxt == want_next, "t15 next_start == next page first_char");
}
free(cps); free(fl); free(fp); free(ml);
}
static void t15(void)
{
/* 固定样本:多段/空段/中英混排/标点/长词/末尾换行 */
t15_check_text("第一章 开始\n\n这是一段正文,包含 English words 与标点。第二句继续填充,"
"直到页面溢出产生多页。\n另一段(左括号测试)右标点,簇!?混合。"
"supercalifragilisticexpialidocious longword\n\n\n末段无换行结尾",
20, 130, 440, 300, "t15 fixed layout");
/* 伪随机长文(确定性种子):40+ 页,覆盖段中页首/页满边界 */
size_t cap = 65536;
char *buf = (char *)malloc(cap);
size_t o = 0;
static const char *cjk[] = { "的","一","是","不","了","人","我","在","有","他","这","中","大",
"来","上","国","个","到","说","们","为","子","和","你","地","出" };
static const char *lat[] = { "a","test","word","x","hello","longwordish","b","qk","reader","zz" };
static const char *pnc[] = { "。",",","、","?","!","(",")"," ", " ", ".", "," };
g_lcg = 0xC0FFEE;
while (o + 16 < cap) {
uint32_t r = lcg() % 100;
const char *s;
if (r < 55) s = cjk[lcg() % (sizeof cjk / sizeof cjk[0])];
else if (r < 80) s = lat[lcg() % (sizeof lat / sizeof lat[0])];
else if (r < 92) s = pnc[lcg() % (sizeof pnc / sizeof pnc[0])];
else if (r < 97) s = "\n";
else s = "\n\n";
size_t sl = strlen(s);
memcpy(buf + o, s, sl); o += sl;
}
buf[o] = 0;
t15_check_text(buf, 20, 130, 440, 640, "t15 random layout A");
t15_check_text(buf, 17, 150, 200, 300, "t15 random layout B"); /* 窄页小字:更多断行/页 */
free(buf);
/* 段中续行起始显式用例:整段一行到底(无 \n),页首必为 mid_para=1 */
{
char *one = (char *)malloc(16384);
size_t k = 0;
for (int i = 0; i < 400; i++) { const char *w = "持续不断的正文"; size_t wl = strlen(w); memcpy(one + k, w, wl); k += wl; }
one[k] = 0;
t15_check_text(one, 20, 130, 440, 640, "t15 single-para layout");
free(one);
}
}
int main(void)
{
t1(); t2(); t3(); t4(); t5();
t6(); t7(); t8(); t9(); t10(); t11(); t12(); t13();
t14(); t15();
printf("test_reader_core: %d pass, %d fail\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}
+83
View File
@@ -0,0 +1,83 @@
/* test/turn_repro.c — 复现「翻页跳页」(R3.2 分段流式)。
* 模拟真机序列:open → (可选 set_enc 到 UTF-16LE) → 跳页/翻页 → 渲染校验(非空/seg 边界正确)。
* 跑于 qemu-mipsel(静态)。
* 用法:qemu-mipsel ./test/turn_repro_mipsel <font.otf> <book.txt> <dirprefix> [enc] [turns]
*/
#include "book.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int render_buf(uint16_t *buf, int W, int H, rd_book *b, int page, uint16_t bg, uint16_t fg)
{
for (int i = 0; i < W * H; i++) buf[i] = bg;
int rc = rd_book_render(b, page, buf, W * 2, bg, fg);
if (rc != 0) return -100 + rc;
int ink = 0;
for (int i = 0; i < W * H; i++) if (buf[i] != bg) ink++;
return ink;
}
int main(int argc, char **argv)
{
const char *font = (argc > 1) ? argv[1] : "/home/Aska/Project/R3ProII/font/src/MiSans-Regular.otf";
const char *book = (argc > 2) ? argv[2] : "/tmp/rdsample/tongyi.txt";
const char *pref = (argc > 3) ? argv[3] : "/tmp/rdsample/repro";
int force_enc = (argc > 4) ? atoi(argv[4]) : 0;
int turns = (argc > 5) ? atoi(argv[5]) : 200;
char segbase[420], idxdir[420];
snprintf(segbase, sizeof segbase, "%s/segbase", pref);
snprintf(idxdir, sizeof idxdir, "%s/index", pref);
rd_book_set_seg_base(segbase);
rd_book_set_index_dir(idxdir);
int fs = 16, ls = 135, pw = 480, ph = 724, mx = 16, my = 20, top = 62;
uint16_t *buf = (uint16_t *)malloc((size_t)pw * ph * 2);
rd_book *b = NULL;
int rc = rd_book_open(&b, book, font, fs, ls, pw, ph, mx, my, top);
printf("open rc=%d enc=%s\n", rc, rc == 0 ? rd_book_encname(b) : "-");
if (rc != 0) return 1;
if (force_enc) { printf("set_enc(%d)->rc=%d\n", force_enc, rd_book_set_enc(b, (rd_enc_t)force_enc)); }
int np = rd_book_pages(b);
printf("npages=%d\n", np);
/* ① 翻页序列:跨段边界观察 cur 跳变 */
rd_book_set_page(b, 2);
int prev = rd_book_current(b), jumps = 0;
for (int i = 0; i < turns; i++) {
int nxt = rd_book_turn(b, 1);
int d = nxt - prev;
if (d != 1) { printf(" JUMP turn#%d %d->%d d=%d npages=%d\n", i, prev, nxt, d, rd_book_pages(b)); if (d > 1) jumps++; }
prev = nxt;
if (rd_book_pages(b) < 1) break;
}
printf("turn jumps>1=%d\n", jumps);
/* ② 渲染首页/第3页/中间页 校验非空 */
int targets[3] = { 0, 2, np > 3 ? np / 2 : 0 };
for (int t = 0; t < 3; t++) {
if (targets[t] >= rd_book_pages(b)) continue;
int ink = render_buf(buf, pw, ph, b, targets[t], 0xFFFF, 0x0000);
printf("render page %d -> %s (ink=%d)\n", targets[t], ink < 0 ? "ERR" : (ink > 50 ? "OK" : "BLANK"), ink);
}
/* ③ set_page 到远页(触发按需补段),渲染校验 + 观察是否落到目标 */
int far = 316;
int got = rd_book_set_page(b, far);
printf("set_page(%d) -> cur=%d npages=%d\n", far, got, rd_book_pages(b));
int ink = render_buf(buf, pw, ph, b, got, 0xFFFF, 0x0000);
printf("render page %d -> %s (ink=%d)\n", got, ink < 0 ? "ERR" : (ink > 50 ? "OK" : "BLANK"), ink);
/* ④ 跳到更远(超过当前 npages,强制补段到 seg 20) */
int far2 = 4000;
int got2 = rd_book_set_page(b, far2);
printf("set_page(%d) -> cur=%d npages=%d\n", far2, got2, rd_book_pages(b));
ink = render_buf(buf, pw, ph, b, got2, 0xFFFF, 0x0000);
printf("render page %d -> %s (ink=%d)\n", got2, ink < 0 ? "ERR" : (ink > 50 ? "OK" : "BLANK"), ink);
free(buf);
rd_book_close(b);
printf("done\n");
return 0;
}
+103
View File
@@ -0,0 +1,103 @@
#!/bin/bash
# tools/check_lgpl_release.sh — 发布前合规校验闸(只读,不改任何产物)。
#
# 用法(在 reader_app 目录外也可):
# ./tools/check_lgpl_release.sh [--skip-net]
#
# 校验项(对齐 devdocs/plan/FUTURE_LIBMOBI.md §4-④ P0):
# 1) os.meow.reader.mpk 必含 resources/licenses/{NOTICE,GPL-3.0.txt,LGPL-3.0.txt,FTL.txt}
# 2) build_reader.sh 锁定的 libmobi commit 与 NOTICE「Locked version」一致(防锁定值漂移)
# 3) [联网] codeload tarball sha256 与 NOTICE 记录指纹一致(未记录则报告,属 P1 补强,不判失败)
# 4) [联网] 主仓 git ls-remote 公开可读(LGPL §4(d) 应用源码可达性)
#
# 退出码:0 = 通过;1 = 硬失败(缺许可证文件 / commit 漂移 / tarball 漂移 / 主仓不可达)。
set -u
cd "$(dirname "$0")/.." # 切到 reader_app 根(build_reader.sh/NOTICE/mpk 都在此)
SKIP_NET=0
[ "${1:-}" = "--skip-net" ] && SKIP_NET=1
MPK="os.meow.reader.mpk"
NOTICE="NOTICE"
BUILD="build_reader.sh"
LIC="resources/licenses"
MAIN_REPO="https://git.yeij.top/AskaEth/MeowOS-For-R3P2.git"
fail=0
say() { printf '%s\n' "$*"; }
hard() { printf 'FAIL %s\n' "$*"; fail=1; }
ok() { printf 'ok %s\n' "$*"; }
warn() { printf 'warn %s\n' "$*"; }
say "== 合规校验(reader_app) =="
# ---------- 1) 包内许可证文件 ----------
if [ ! -f "$MPK" ]; then
hard "找不到 $MPK —— 请先 ./build_reader.sh 再 ./pack_reader_mpk.sh"
else
for f in NOTICE GPL-3.0.txt LGPL-3.0.txt FTL.txt; do
if tar tzf "$MPK" 2>/dev/null | grep -qx "resources/licenses/$f"; then
ok "包内含 resources/licenses/$f"
else
hard "包内缺 resources/licenses/$f"
fi
done
fi
# ---------- 2) libmobi 锁定 commit 一致性 ----------
LM_URL="$(sed -n 's/^LM_URL="\([^"]*\)"/\1/p' "$BUILD" | head -1)"
LM_COMMIT="$(printf '%s' "$LM_URL" | sed 's#.*/##')"
NOTICE_COMMIT="$(sed -n 's/^[[:space:]]*\([0-9a-f]\{40\}\).*/\1/p' "$NOTICE" | head -1)"
if [ -z "$LM_COMMIT" ]; then
hard "build_reader.sh 未解析到 LM_URL 锁定 commit"
elif [ "$LM_COMMIT" != "$NOTICE_COMMIT" ]; then
hard "libmobi 锁定 commit 漂移:build=$LM_COMMIT NOTICE=$NOTICE_COMMIT"
else
ok "libmobi 锁定 commit 一致 ($LM_COMMIT)"
fi
# ---------- 3) [联网] tarball sha256 ----------
NOTICE_SHA="$(sed -n 's/^[[:space:]]*Tarball sha256[^:]*:[[:space:]]*\([0-9a-f]\{64\}\).*/\1/p' "$NOTICE" | head -1)"
LIVE_SHA=""
if [ "$SKIP_NET" = 1 ]; then
warn "跳过在线 tarball 校验 (--skip-net)"
if [ -n "$NOTICE_SHA" ]; then ok "NOTICE 已记录 tarball sha256 ($NOTICE_SHA)"; else warn "NOTICE 未记录 tarball sha256(P1 补强项)"; fi
else
if command -v curl >/dev/null 2>&1 && command -v sha256sum >/dev/null 2>&1 && [ -n "$LM_URL" ]; then
LIVE_SHA="$(curl -fsSL "$LM_URL" 2>/dev/null | sha256sum | awk '{print $1}')"
if [ -z "$LIVE_SHA" ]; then
warn "在线取 tarball 失败(网络不可用)—— 未校验 sha256"
elif [ -n "$NOTICE_SHA" ]; then
if [ "$LIVE_SHA" = "$NOTICE_SHA" ]; then ok "tarball sha256 与 NOTICE 一致 ($LIVE_SHA)"; else hard "tarball sha256 漂移:live=$LIVE_SHA NOTICE=$NOTICE_SHA"; fi
else
say "info tarball sha256=$LIVE_SHA (NOTICE 未记录指纹,属 P1 补强项,不判失败)"
fi
else
warn "无 curl/sha256sum 或未解析 LM_URL —— 跳过在线 tarball 校验"
fi
fi
# ---------- 4) [联网] 主仓公开可读 ----------
if [ "$SKIP_NET" = 1 ]; then
warn "跳过主仓可读性校验 (--skip-net)"
else
if command -v git >/dev/null 2>&1; then
if git ls-remote "$MAIN_REPO" HEAD >/dev/null 2>&1; then
ok "主仓公开可读 (git ls-remote HEAD)"
else
hard "主仓 git ls-remote 失败(LGPL §4(d) 应用源码可达性)"
fi
else
warn "无 git —— 跳过主仓可读性校验"
fi
fi
# ---------- 5) 合规清单 ----------
say "== 合规清单 =="
say "libmobi commit : ${LM_COMMIT:-?}"
say "libmobi tarball : ${LIVE_SHA:-${NOTICE_SHA:-未校验}}"
say "包内许可证 :"
if [ -f "$MPK" ]; then tar tzf "$MPK" 2>/dev/null | grep 'resources/licenses/' || true; fi
if [ "$fail" = 1 ]; then say "结果:FAIL"; exit 1; fi
say "结果:PASS"