Merge branch 'feat/m643-reader-streaming-panel-sync'
This commit is contained in:
commit
e87ccdf061
19 files changed
+1312
-60
No files matched your search
@@ -304,6 +304,7 @@ void ams_history_parse(char *out, sbuf_t *s, const char *pkg);
|
|||||||
void ams_history_fill(sbuf_t *s, const char *pkg);
|
void ams_history_fill(sbuf_t *s, const char *pkg);
|
||||||
int api_ams_history(int sock, const char *pkg);
|
int api_ams_history(int sock, const char *pkg);
|
||||||
int api_ams_ctl(int sock, const char *pkg, int kill);
|
int api_ams_ctl(int sock, const char *pkg, int kill);
|
||||||
|
int api_ams_kill_fg(int sock, const char *pkg); /* 前台强制终止(救命):am kill 优先,launcher 拒→直接 pid 信号 */
|
||||||
void svcd_fill(sbuf_t *s);
|
void svcd_fill(sbuf_t *s);
|
||||||
int api_svcd(int sock);
|
int api_svcd(int sock);
|
||||||
int api_overview(int sock);
|
int api_overview(int sock);
|
||||||
@@ -446,7 +447,9 @@ int api_screen_jpeg(int sock, const req_t *r);
|
|||||||
int64_t now_mono_ms(void);
|
int64_t now_mono_ms(void);
|
||||||
int64_t touch_last_local_ms(const char *dev);
|
int64_t touch_last_local_ms(const char *dev);
|
||||||
int inject_tap(const char *dev, int x, int y);
|
int inject_tap(const char *dev, int x, int y);
|
||||||
|
int inject_swipe(const char *dev, int x0, int y0, int x1, int y1);
|
||||||
int api_input_tap(int sock, const req_t *r);
|
int api_input_tap(int sock, const req_t *r);
|
||||||
|
int api_input_gesture(int sock, const req_t *r);
|
||||||
const char *web_root(void);
|
const char *web_root(void);
|
||||||
const char *mime_for(const char *path);
|
const char *mime_for(const char *path);
|
||||||
int serve_static(int sock, const char *path);
|
int serve_static(int sock, const char *path);
|
||||||
|
|||||||
+147
-19
@@ -253,7 +253,32 @@ static void bg_proc_sample(int pid, double *pcpu, long *rss_kb)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 解析已 strip 的 `am list` 全文 → bg 列表 JSON(state=bg 的 APP 行)。 */
|
/* 发射单条 APP 行 JSON(复用于 bg/fg 列表;带 state + is_fg + cpu_pct/rss_kb)。
|
||||||
|
* toks 为已切分的 `APP ...` 行(n>=9);*first 控制数组元素逗号(调用方初始化为 1)。
|
||||||
|
* cpu_pct/rss_kb 诚实原则:pid 读不到/不作为可用进程 → null(前端显「—」),绝不编造。 */
|
||||||
|
static void ams_entry_emit(sbuf_t *out, char **toks, int n, int *first)
|
||||||
|
{
|
||||||
|
const char *st = toks[n - 6]; /* 自右向左:state=tok[n-6], pid=n-5(与 ams_fill 同布局) */
|
||||||
|
char nm[128];
|
||||||
|
join_tokens(toks, 2, n - 6, nm, sizeof nm);
|
||||||
|
int pid = atoi(toks[n - 5]);
|
||||||
|
double pcpu = -1.0; long rss = -1;
|
||||||
|
if (pid > 0) bg_proc_sample(pid, &pcpu, &rss);
|
||||||
|
if (!*first) sA(out, ",");
|
||||||
|
*first = 0;
|
||||||
|
sA(out, "{\"pkg\":"); sA_str(out, toks[1]);
|
||||||
|
sA(out, ",\"name\":"); sA_str(out, nm);
|
||||||
|
sA(out, ",\"pid\":%d", pid);
|
||||||
|
sA(out, ",\"state\":"); sA_str(out, st);
|
||||||
|
sA(out, ",\"is_fg\":%s", strcmp(st, "fg") == 0 ? "true" : "false");
|
||||||
|
sA(out, ",\"cpu_pct\":");
|
||||||
|
if (pcpu < 0) sA(out, "null"); else sA(out, "%.1f", pcpu);
|
||||||
|
sA(out, ",\"rss_kb\":");
|
||||||
|
if (rss < 0) sA(out, "null"); else sA(out, "%ld", rss);
|
||||||
|
sA(out, "}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 解析已 strip 的 `am list` 全文 → bg 列表 JSON(state=bg 的 APP 行;向后兼容专用,selftest 覆盖)。 */
|
||||||
void ams_bg_parse(char *list_text, sbuf_t *s)
|
void ams_bg_parse(char *list_text, sbuf_t *s)
|
||||||
{
|
{
|
||||||
char *toks[256];
|
char *toks[256];
|
||||||
@@ -266,25 +291,10 @@ void ams_bg_parse(char *list_text, sbuf_t *s)
|
|||||||
if (strncmp(ln, "APP ", 4) == 0) {
|
if (strncmp(ln, "APP ", 4) == 0) {
|
||||||
int n = txt_tokens(ln, toks, 256);
|
int n = txt_tokens(ln, toks, 256);
|
||||||
if (n >= 9) {
|
if (n >= 9) {
|
||||||
/* 自右向左固定(与 ams_fill 同布局):state=tok[n-6], pid=n-5;pkg=tok[1],name=join(tok[2..n-6)) */
|
|
||||||
const char *st = toks[n - 6];
|
const char *st = toks[n - 6];
|
||||||
if (strcmp(st, "bg") == 0) {
|
if (strcmp(st, "bg") == 0) {
|
||||||
char nm[128];
|
count++;
|
||||||
join_tokens(toks, 2, n - 6, nm, sizeof nm);
|
ams_entry_emit(&apps, toks, n, &first);
|
||||||
int pid = atoi(toks[n - 5]);
|
|
||||||
double pcpu; long rss;
|
|
||||||
bg_proc_sample(pid, &pcpu, &rss);
|
|
||||||
if (!first) sA(&apps, ",");
|
|
||||||
first = 0; count++;
|
|
||||||
sA(&apps, "{\"pkg\":"); sA_str(&apps, toks[1]);
|
|
||||||
sA(&apps, ",\"name\":"); sA_str(&apps, nm);
|
|
||||||
sA(&apps, ",\"pid\":%d", pid);
|
|
||||||
sA(&apps, ",\"state\":"); sA_str(&apps, st);
|
|
||||||
sA(&apps, ",\"cpu_pct\":");
|
|
||||||
if (pcpu < 0) sA(&apps, "null"); else sA(&apps, "%.1f", pcpu);
|
|
||||||
sA(&apps, ",\"rss_kb\":");
|
|
||||||
if (rss < 0) sA(&apps, "null"); else sA(&apps, "%ld", rss);
|
|
||||||
sA(&apps, "}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,6 +305,8 @@ void ams_bg_parse(char *list_text, sbuf_t *s)
|
|||||||
sbuf_free(&apps);
|
sbuf_free(&apps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* am list 全文 → bg+fg 双列表 JSON。`apps`(bg)保持向后兼容(count=bg 数量);
|
||||||
|
* 追加 `fg` + `fg_count`(前台 App,前端救命分组)。state=stop 无进程,不在本页(proc 页可见)。 */
|
||||||
void ams_bg_fill(sbuf_t *s)
|
void ams_bg_fill(sbuf_t *s)
|
||||||
{
|
{
|
||||||
char out[16384];
|
char out[16384];
|
||||||
@@ -303,7 +315,28 @@ void ams_bg_fill(sbuf_t *s)
|
|||||||
int code = run_cmd(cmd, out, sizeof out);
|
int code = run_cmd(cmd, out, sizeof out);
|
||||||
if (code != 0 || !strstr(out, "APP")) { sA(s, "{\"available\":false}"); return; }
|
if (code != 0 || !strstr(out, "APP")) { sA(s, "{\"available\":false}"); return; }
|
||||||
panel_strip_ansi(out);
|
panel_strip_ansi(out);
|
||||||
ams_bg_parse(out, s);
|
char *toks[256];
|
||||||
|
sbuf_t bg = {0}, fg = {0};
|
||||||
|
int bg_first = 1, fg_first = 1, bg_count = 0, fg_count = 0;
|
||||||
|
char *ln = out;
|
||||||
|
while (*ln) {
|
||||||
|
char *nl = strchr(ln, '\n');
|
||||||
|
if (nl) *nl = 0;
|
||||||
|
if (strncmp(ln, "APP ", 4) == 0) {
|
||||||
|
int n = txt_tokens(ln, toks, 256);
|
||||||
|
if (n >= 9) {
|
||||||
|
const char *st = toks[n - 6];
|
||||||
|
if (strcmp(st, "bg") == 0) { bg_count++; ams_entry_emit(&bg, toks, n, &bg_first); }
|
||||||
|
else if (strcmp(st, "fg") == 0) { fg_count++; ams_entry_emit(&fg, toks, n, &fg_first); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||||||
|
}
|
||||||
|
sA(s, "{\"available\":true,\"count\":%d,\"ts\":%ld,\"apps\":[%s]",
|
||||||
|
bg_count, (long)time(NULL), bg.b ? bg.b : "");
|
||||||
|
sA(s, ",\"fg_count\":%d,\"fg\":[%s]}", fg_count, fg.b ? fg.b : "");
|
||||||
|
sbuf_free(&bg);
|
||||||
|
sbuf_free(&fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
int api_ams_bg(int sock)
|
int api_ams_bg(int sock)
|
||||||
@@ -314,3 +347,98 @@ int api_ams_bg(int sock)
|
|||||||
sbuf_free(&s);
|
sbuf_free(&s);
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 从 am list 解析某 pkg 的当前 pid(未注册/无进程 → 0)。 */
|
||||||
|
static int ams_pkg_pid(const char *pkg)
|
||||||
|
{
|
||||||
|
char out[16384];
|
||||||
|
char cmd[300];
|
||||||
|
snprintf(cmd, sizeof cmd, "%s list 2>/dev/null", ams_bin());
|
||||||
|
if (run_cmd(cmd, out, sizeof out) != 0 || !strstr(out, "APP")) return 0;
|
||||||
|
panel_strip_ansi(out);
|
||||||
|
char *toks[256];
|
||||||
|
char *ln = out;
|
||||||
|
while (*ln) {
|
||||||
|
char *nl = strchr(ln, '\n');
|
||||||
|
if (nl) *nl = 0;
|
||||||
|
if (strncmp(ln, "APP ", 4) == 0) {
|
||||||
|
int n = txt_tokens(ln, toks, 256);
|
||||||
|
if (n >= 9 && strcmp(toks[1], pkg) == 0) {
|
||||||
|
int pid = atoi(toks[n - 5]);
|
||||||
|
if (nl) *nl = '\n';
|
||||||
|
return pid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* /proc/<pid>/stat 进程是否已消失/僵尸:-1=已消失,1=僵尸,0=存活。 */
|
||||||
|
static int proc_zombie(int pid)
|
||||||
|
{
|
||||||
|
char path[64], line[256];
|
||||||
|
snprintf(path, sizeof path, "/proc/%d/stat", pid);
|
||||||
|
if (read_small_file(path, line, sizeof line) < 0) return -1;
|
||||||
|
char *rp = strrchr(line, ')');
|
||||||
|
if (!rp) return -1;
|
||||||
|
const char *p = rp + 2;
|
||||||
|
while (*p == ' ' || *p == '\t') p++;
|
||||||
|
return (*p == 'Z') ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 前台强制终止(救命):优先走 `am kill`(launcher 干净 SIGTERM 序列,accepted);若 launcher 拒
|
||||||
|
* (framework / 无前台会话 → "launcher refused"),回退:解析 am list 拿当前 pid → 直接 SIGTERM,
|
||||||
|
* 宽限后仍存活走 SIGKILL(与 launcher 杀序列同语义)。这是用户明示「前台卡死救命」的强通道——
|
||||||
|
* 前端已二次确认 + 标注「前台」。require_auth 在路由层已护。 */
|
||||||
|
int api_ams_kill_fg(int sock, const char *pkg)
|
||||||
|
{
|
||||||
|
if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||||||
|
|
||||||
|
/* ① 干净通道:am kill(launcher 执行 SIGTERM 序列)。 */
|
||||||
|
{
|
||||||
|
char out[256];
|
||||||
|
char cmd[320];
|
||||||
|
snprintf(cmd, sizeof cmd, "%s kill %s 2>/dev/null", ams_bin(), pkg);
|
||||||
|
int code = run_cmd(cmd, out, sizeof out);
|
||||||
|
if (code == 0 && strstr(out, "OK ")) {
|
||||||
|
sbuf_t s = {0};
|
||||||
|
sA(&s, "{\"accepted\":true,\"op\":\"kill-fg\",\"pkg\":");
|
||||||
|
sA_str(&s, pkg);
|
||||||
|
sA(&s, ",\"force\":false,\"message\":"); sA_str(&s, out);
|
||||||
|
sA(&s, "}");
|
||||||
|
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||||
|
sbuf_free(&s);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
panel_log("ams kill-fg %s via am refused: %s -> direct signal fallback", pkg,
|
||||||
|
out[0] ? out : "(empty)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ② 回退:直接按 pid 信号(绕过 launcher 门控)。 */
|
||||||
|
int pid = ams_pkg_pid(pkg);
|
||||||
|
if (pid <= 0) return send_err(sock, 409, "CONFLICT", "该 App 当前无进程可终止");
|
||||||
|
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
int killed_hard = 0;
|
||||||
|
for (int i = 0; i < 5; i++) { /* SIGTERM 宽限 ~0.6s(与 launcher 杀序列同语义) */
|
||||||
|
struct timespec ts = {0, 120 * 1000 * 1000};
|
||||||
|
nanosleep(&ts, NULL);
|
||||||
|
int z = proc_zombie(pid);
|
||||||
|
if (z < 0 || z == 1) break; /* 消失/僵尸 → 已终止 */
|
||||||
|
}
|
||||||
|
if (proc_zombie(pid) == 0) { kill(pid, SIGKILL); killed_hard = 1; }
|
||||||
|
|
||||||
|
char msg[192];
|
||||||
|
snprintf(msg, sizeof msg, "force terminated pid %d via direct signal%s", pid,
|
||||||
|
killed_hard ? " (SIGKILL 强杀)" : " (SIGTERM)");
|
||||||
|
sbuf_t s = {0};
|
||||||
|
sA(&s, "{\"accepted\":true,\"op\":\"kill-fg\",\"pkg\":");
|
||||||
|
sA_str(&s, pkg);
|
||||||
|
sA(&s, ",\"force\":%s,\"pid\":%d,\"message\":", killed_hard ? "true" : "false", pid);
|
||||||
|
sA_str(&s, msg);
|
||||||
|
sA(&s, "}");
|
||||||
|
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||||
|
sbuf_free(&s);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
/* mp_screen.c — 实时画面域:/api/screen.jpeg(fb0→JPEG,增量帧)+ 触摸注入 /api/input/tap。 */
|
/* mp_screen.c — 实时画面域:/api/screen.jpeg(fb0→JPEG,增量帧)+ 触摸注入 /api/input/tap
|
||||||
|
* 与系统手势注入 /api/input/gesture(back=左缘右滑 / home=底部上滑回桌面)。 */
|
||||||
#include "mp.h"
|
#include "mp.h"
|
||||||
|
|
||||||
static uint8_t *g_screen_prev = NULL; /* 上一帧紧凑 RGB565 */
|
static uint8_t *g_screen_prev = NULL; /* 上一帧紧凑 RGB565 */
|
||||||
@@ -6,6 +7,17 @@ static int g_screen_pw = 0, g_screen_ph = 0;
|
|||||||
static int64_t g_last_self_inject_ms = 0;
|
static int64_t g_last_self_inject_ms = 0;
|
||||||
#define TOUCH_ACTIVE_MS 5000
|
#define TOUCH_ACTIVE_MS 5000
|
||||||
|
|
||||||
|
/* 手势注入阈值(镜像 meow_ui_input.c / np_gesture.h 系统已识别手势,勿新造语义):
|
||||||
|
* 返回=左缘(起)右移 ≥SWIPE_THRESH 且横向主导;回桌面=底部热区(起)上移 ≥HOME_SWIPE
|
||||||
|
* 且竖向主导 + down_x≥EDGE_W(避左缘返回列)。此处位移留容差(≥65)。 */
|
||||||
|
#define GESTURE_EDGE_W 24 /* 左缘宽度:返回手势起点 x 须 < 此值(meow_ui_input.c EDGE_W) */
|
||||||
|
#define GESTURE_SWIPE_DX 78 /* 返回手势右移位移(≥ SDK SWIPE_THRESH 65,留容差) */
|
||||||
|
#define GESTURE_HOME_EDGE_H 28 /* 底部热区高:回桌面起点 y 须 >= fb_h-此值(HOME_EDGE_H) */
|
||||||
|
#define GESTURE_HOME_DY 80 /* 回桌面手势上移位移(≥ SDK HOME_SWIPE 65,留容差) */
|
||||||
|
#define GESTURE_STEPS 10 /* 线性轨迹插值点数(营造真实滑动轨迹) */
|
||||||
|
#define GESTURE_STEP_MS 12 /* 每步间隔毫秒 */
|
||||||
|
#define GESTURE_HOLD_MS 60 /* 末端按住毫秒再抬起(防 fling 误判) */
|
||||||
|
|
||||||
|
|
||||||
int screen_read_frame(const char *dev, uint8_t **out, int *w, int *h)
|
int screen_read_frame(const char *dev, uint8_t **out, int *w, int *h)
|
||||||
{
|
{
|
||||||
@@ -230,6 +242,47 @@ int inject_tap(const char *dev, int x, int y)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 线性滑动注入:按下(原点) → 插值移动 → 末端按住 → 抬起。事件序同 inject_tap
|
||||||
|
* (BTN_TOUCH 边沿先于 ABS,goodix 时序免疫;首帧锁原点),设备手势机据此判
|
||||||
|
* 「左缘右滑=返回」或「底部上滑=回桌面」(复用系统已识别信号,不新造)。 */
|
||||||
|
int inject_swipe(const char *dev, int x0, int y0, int x1, int y1)
|
||||||
|
{
|
||||||
|
int fd = open(dev, O_WRONLY);
|
||||||
|
if (fd < 0) return -1;
|
||||||
|
struct input_event ev;
|
||||||
|
#define __WE(t_, c_, v_) do { memset(&ev, 0, sizeof ev); ev.type = (t_); ev.code = (c_); \
|
||||||
|
ev.value = (v_); if (write(fd, &ev, sizeof ev) != (ssize_t)sizeof ev) { close(fd); return -1; } } while (0)
|
||||||
|
/* 按下边沿 + 首帧 ABS 锁原点(与 tap 同序) */
|
||||||
|
__WE(EV_KEY, BTN_TOUCH, 1);
|
||||||
|
__WE(EV_ABS, ABS_MT_SLOT, 0);
|
||||||
|
__WE(EV_ABS, ABS_MT_TRACKING_ID, 7);
|
||||||
|
__WE(EV_ABS, ABS_MT_POSITION_X, x0);
|
||||||
|
__WE(EV_ABS, ABS_MT_POSITION_Y, y0);
|
||||||
|
__WE(EV_ABS, ABS_MT_TOUCH_MAJOR, 6);
|
||||||
|
__WE(EV_ABS, ABS_X, x0);
|
||||||
|
__WE(EV_ABS, ABS_Y, y0);
|
||||||
|
__WE(EV_SYN, SYN_REPORT, 0);
|
||||||
|
/* 插值逐点移动:每步坐标更新 + 同步,营造连续轨迹 */
|
||||||
|
for (int i = 1; i <= GESTURE_STEPS; i++) {
|
||||||
|
int xi = x0 + (x1 - x0) * i / GESTURE_STEPS;
|
||||||
|
int yi = y0 + (y1 - y0) * i / GESTURE_STEPS;
|
||||||
|
__WE(EV_ABS, ABS_MT_POSITION_X, xi);
|
||||||
|
__WE(EV_ABS, ABS_MT_POSITION_Y, yi);
|
||||||
|
__WE(EV_ABS, ABS_X, xi);
|
||||||
|
__WE(EV_ABS, ABS_Y, yi);
|
||||||
|
__WE(EV_SYN, SYN_REPORT, 0);
|
||||||
|
usleep((useconds_t)GESTURE_STEP_MS * 1000);
|
||||||
|
}
|
||||||
|
/* 末端短暂按住再抬起(与 tap 同:抬起到按下边沿被手势机视为一次抬手) */
|
||||||
|
usleep((useconds_t)GESTURE_HOLD_MS * 1000);
|
||||||
|
__WE(EV_KEY, BTN_TOUCH, 0);
|
||||||
|
__WE(EV_ABS, ABS_MT_TRACKING_ID, -1);
|
||||||
|
__WE(EV_SYN, SYN_REPORT, 0);
|
||||||
|
#undef __WE
|
||||||
|
close(fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int api_input_tap(int sock, const req_t *r)
|
int api_input_tap(int sock, const req_t *r)
|
||||||
{
|
{
|
||||||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||||
@@ -275,3 +328,71 @@ int api_input_tap(int sock, const req_t *r)
|
|||||||
sbuf_free(&s);
|
sbuf_free(&s);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* /api/input/gesture:注入设备已识别的系统手势(back=左缘右滑 / home=底部上滑回桌面)。
|
||||||
|
* 复用现有触摸注入通道(/dev/input/event1 滑动轨迹),不触碰被物理键红线禁注入的
|
||||||
|
* /api/key 158/172;由设备自身手势机判成 MEOW_SWIPE_LEFT / MEOW_EV_HOME。 */
|
||||||
|
int api_input_gesture(int sock, const req_t *r)
|
||||||
|
{
|
||||||
|
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||||
|
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||||||
|
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||||
|
jv_t *jg = jv_obj_get(root, "gesture");
|
||||||
|
if (!jg || jg->type != J_STR) {
|
||||||
|
jv_free(root);
|
||||||
|
return send_err(sock, 400, "BAD_REQUEST", "缺 gesture(back|home)");
|
||||||
|
}
|
||||||
|
const char *gesture = jg->str;
|
||||||
|
int is_back = (strcmp(gesture, "back") == 0);
|
||||||
|
int is_home = (strcmp(gesture, "home") == 0);
|
||||||
|
jv_free(root);
|
||||||
|
if (!is_back && !is_home)
|
||||||
|
return send_err(sock, 400, "BAD_REQUEST", "gesture 非法(仅 back|home)");
|
||||||
|
|
||||||
|
const char *dev = getenv("MEOW_TOUCH_DEV"); if (!dev || !dev[0]) dev = TOUCH_DEV;
|
||||||
|
const char *fbdev = getenv("MEOW_FB_DEV"); if (!fbdev || !fbdev[0]) fbdev = FB_DEV;
|
||||||
|
int sw = 480, sh = 720;
|
||||||
|
{ int fx = 0, fy = 0; if (fb_dims(fbdev, &fx, &fy) == 0) { sw = fx; sh = fy; } }
|
||||||
|
if (sw < 1) sw = 480;
|
||||||
|
if (sh < 1) sh = 720;
|
||||||
|
|
||||||
|
/* 几何(镜像 SDK 判据 + 留容差,见上 GESTURE_* 宏) */
|
||||||
|
int x0 = 0, y0 = 0, x1 = 0, y1 = 0;
|
||||||
|
if (is_back) {
|
||||||
|
x0 = 4; /* 左缘内(< EDGE_W=24) */
|
||||||
|
if (x0 >= sw) x0 = sw - 1;
|
||||||
|
if (x0 >= GESTURE_EDGE_W) x0 = GESTURE_EDGE_W - 1;
|
||||||
|
y0 = y1 = sh / 2; /* 屏中部,避开顶部下拉/底部上滑热区 */
|
||||||
|
x1 = x0 + GESTURE_SWIPE_DX;
|
||||||
|
if (x1 >= sw) x1 = sw - 1; /* 横向主导 + 右移≥阈值 */
|
||||||
|
} else { /* home */
|
||||||
|
x0 = x1 = sw / 2; /* 屏底中部,避开左缘返回列 */
|
||||||
|
if (x0 < GESTURE_EDGE_W) x0 = x1 = GESTURE_EDGE_W;
|
||||||
|
y0 = sh - 2; /* 底部热区(>= fb_h-HOME_EDGE_H) */
|
||||||
|
if (y0 >= sh) y0 = sh - 1;
|
||||||
|
y1 = y0 - GESTURE_HOME_DY;
|
||||||
|
if (y1 < 0) y1 = 0; /* 竖向主导 + 上移≥阈值 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 仲裁:本机最近触摸后 5s 内忽略注入(与 tap 同) */
|
||||||
|
int64_t now = now_mono_ms();
|
||||||
|
int64_t last = touch_last_local_ms(dev);
|
||||||
|
if (last > 0 && (now - last) < TOUCH_ACTIVE_MS) {
|
||||||
|
sbuf_t s = {0};
|
||||||
|
sA(&s, "{\"dispatched\":false,\"reason\":\"local_touch_active\",\"cooldown_ms\":%lld}",
|
||||||
|
(long long)(TOUCH_ACTIVE_MS - (now - last)));
|
||||||
|
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||||||
|
sbuf_free(&s);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_last_self_inject_ms = now_mono_ms();
|
||||||
|
if (inject_swipe(dev, x0, y0, x1, y1) < 0)
|
||||||
|
return send_err(sock, 500, "INTERNAL", "手势注入失败(无法打开触摸设备)");
|
||||||
|
sbuf_t s = {0};
|
||||||
|
sA(&s, "{\"dispatched\":true,\"gesture\":\"%s\",\"from\":[%d,%d],\"to\":[%d,%d]}",
|
||||||
|
gesture, x0, y0, x1, y1);
|
||||||
|
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||||||
|
sbuf_free(&s);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
@@ -339,16 +339,22 @@ int handle(int sock, req_t *r)
|
|||||||
if (!strcmp(p, "/api/audio/volume")) return api_audio_volume(sock, r->body, r->body_len);
|
if (!strcmp(p, "/api/audio/volume")) return api_audio_volume(sock, r->body, r->body_len);
|
||||||
if (!strcmp(p, "/api/key")) return api_key_batch(sock, r);
|
if (!strcmp(p, "/api/key")) return api_key_batch(sock, r);
|
||||||
if (!strcmp(p, "/api/input/tap")) return api_input_tap(sock, r);
|
if (!strcmp(p, "/api/input/tap")) return api_input_tap(sock, r);
|
||||||
|
if (!strcmp(p, "/api/input/gesture")) return api_input_gesture(sock, r);
|
||||||
char seg[256];
|
char seg[256];
|
||||||
if (path_segment("/api/key/", p, seg, sizeof seg))
|
if (path_segment("/api/key/", p, seg, sizeof seg))
|
||||||
return api_key_single(sock, atoi(seg), r);
|
return api_key_single(sock, atoi(seg), r);
|
||||||
/* POST /api/ams/<pkg>/start|kill(管启/停;require_auth 已护,kill 前台前端二次确认) */
|
/* POST /api/ams/<pkg>/start|kill|killfg(管启/停;require_auth 已护,kill 前台前端二次确认;
|
||||||
|
* killfg=前台救命强制终止:am kill 优先,launcher 拒→直接 pid 信号,前端二次确认+标注前台) */
|
||||||
if (!strncmp(p, "/api/ams/", 9)) {
|
if (!strncmp(p, "/api/ams/", 9)) {
|
||||||
size_t pl = strlen(p);
|
size_t pl = strlen(p);
|
||||||
if (pl > 5 && !strcmp(p + pl - 6, "/start")) {
|
if (pl > 5 && !strcmp(p + pl - 6, "/start")) {
|
||||||
char pkg[256];
|
char pkg[256];
|
||||||
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
||||||
return api_ams_ctl(sock, pkg, 0);
|
return api_ams_ctl(sock, pkg, 0);
|
||||||
|
} else if (pl > 7 && !strcmp(p + pl - 7, "/killfg")) {
|
||||||
|
char pkg[256];
|
||||||
|
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
||||||
|
return api_ams_kill_fg(sock, pkg);
|
||||||
} else if (pl > 4 && !strcmp(p + pl - 5, "/kill")) {
|
} else if (pl > 4 && !strcmp(p + pl - 5, "/kill")) {
|
||||||
char pkg[256];
|
char pkg[256];
|
||||||
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<span class="nav-ico"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="9" cy="6" r="2.2" fill="var(--bg)"/><circle cx="15" cy="12" r="2.2" fill="var(--bg)"/><circle cx="7" cy="18" r="2.2" fill="var(--bg)"/></svg></span>
|
<span class="nav-ico"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="9" cy="6" r="2.2" fill="var(--bg)"/><circle cx="15" cy="12" r="2.2" fill="var(--bg)"/><circle cx="7" cy="18" r="2.2" fill="var(--bg)"/></svg></span>
|
||||||
<span class="nav-label">进程·服务</span>
|
<span class="nav-label">进程·服务</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-item" data-route="bg" title="后台活动(App 后台常驻:CPU/内存占用 + 一键清后台)">
|
<button class="nav-item" data-route="bg" title="App 活动(前台可终止救命 · 后台常驻:CPU/内存占用 + 一键清后台)">
|
||||||
<span class="nav-ico"><svg viewBox="0 0 24 24"><path d="M12 3 3 8l9 5 9-5-9-5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M3 12l9 5 9-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M3 16l9 5 9-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg></span>
|
<span class="nav-ico"><svg viewBox="0 0 24 24"><path d="M12 3 3 8l9 5 9-5-9-5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M3 12l9 5 9-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M3 16l9 5 9-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg></span>
|
||||||
<span class="nav-label">后台</span>
|
<span class="nav-label">后台</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -283,6 +283,9 @@ input[type=range]{ width:100%; accent-color:var(--primary); }
|
|||||||
.bg-row .bg-name{ font-weight:600; }
|
.bg-row .bg-name{ font-weight:600; }
|
||||||
.bg-row .bg-sub{ font-size:12px; color:var(--on-surface-variant); margin-top:2px; display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
|
.bg-row .bg-sub{ font-size:12px; color:var(--on-surface-variant); margin-top:2px; display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
|
||||||
.bg-row .btn{ flex:0 0 auto; }
|
.bg-row .btn{ flex:0 0 auto; }
|
||||||
|
.bg-group{ margin:14px 0 8px; font-weight:600; font-size:13px; color:var(--on-surface-variant); }
|
||||||
|
.bg-group:first-of-type{ margin-top:0; }
|
||||||
|
.bg-group + .bg-list{ margin-bottom:4px; }
|
||||||
|
|
||||||
/* 实时屏幕(屏幕页) */
|
/* 实时屏幕(屏幕页) */
|
||||||
.screen-frame{ display:flex; justify-content:center; max-width:480px; margin:0 auto; }
|
.screen-frame{ display:flex; justify-content:center; max-width:480px; margin:0 auto; }
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
notif: { title: '通知中心', render: renderNotif, bind: bindNotif },
|
notif: { title: '通知中心', render: renderNotif, bind: bindNotif },
|
||||||
power: { title: '电源 / 功耗', render: renderPower, bind: bindPower },
|
power: { title: '电源 / 功耗', render: renderPower, bind: bindPower },
|
||||||
proc: { title: '进程 / 服务', render: renderProc, bind: bindProc },
|
proc: { title: '进程 / 服务', render: renderProc, bind: bindProc },
|
||||||
bg: { title: '后台管理', render: renderBg, bind: bindBg }
|
bg: { title: 'App 活动(前台可终止 · 后台管理)', render: renderBg, bind: bindBg }
|
||||||
};
|
};
|
||||||
let started = false;
|
let started = false;
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
power: '<svg viewBox="0 0 24 24"><path d="M12 3v8" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M6.6 6.6a7.5 7.5 0 1 0 10.8 0" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>',
|
power: '<svg viewBox="0 0 24 24"><path d="M12 3v8" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M6.6 6.6a7.5 7.5 0 1 0 10.8 0" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>',
|
||||||
play: '<svg viewBox="0 0 24 24"><path d="M7 5.5v13l11-6.5z" fill="currentColor"/></svg>',
|
play: '<svg viewBox="0 0 24 24"><path d="M7 5.5v13l11-6.5z" fill="currentColor"/></svg>',
|
||||||
next: '<svg viewBox="0 0 24 24"><path d="M5 5.5l9 6.5-9 6.5z" fill="currentColor"/><rect x="16.2" y="5" width="2.4" height="14" fill="currentColor"/></svg>',
|
next: '<svg viewBox="0 0 24 24"><path d="M5 5.5l9 6.5-9 6.5z" fill="currentColor"/><rect x="16.2" y="5" width="2.4" height="14" fill="currentColor"/></svg>',
|
||||||
prev: '<svg viewBox="0 0 24 24"><path d="M19 5.5l-9 6.5 9 6.5z" fill="currentColor"/><rect x="5.4" y="5" width="2.4" height="14" fill="currentColor"/></svg>'
|
prev: '<svg viewBox="0 0 24 24"><path d="M19 5.5l-9 6.5 9 6.5z" fill="currentColor"/><rect x="5.4" y="5" width="2.4" height="14" fill="currentColor"/></svg>',
|
||||||
|
back: '<svg viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" fill="currentColor"/></svg>',
|
||||||
|
home: '<svg viewBox="0 0 24 24"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" fill="currentColor"/></svg>'
|
||||||
};
|
};
|
||||||
let ctlBright = { min: 1, max: 100, value: 100 }; // 控制板亮度(读取自 /api/settings/display.brightness)
|
let ctlBright = { min: 1, max: 100, value: 100 }; // 控制板亮度(读取自 /api/settings/display.brightness)
|
||||||
let logsGroup = 'all', logsPrefix = '';
|
let logsGroup = 'all', logsPrefix = '';
|
||||||
@@ -889,9 +891,10 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 批④ 后台活动页(#/bg):列表 + 行内「杀死」 + 顶栏「一键清后台」(复用 /api/ams/bg 只读 + /api/ams/:pkg/kill) ---- */
|
/* ---- 批④ App 活动页(#/bg):前台「终止」救命 + 后台列表/行内「杀死」 + 顶栏「一键清后台」
|
||||||
|
*(复用 /api/ams/bg 只读:apps=bg 向后兼容 + fg=前台;前台终止走 /api/ams/:pkg/killfg,后台杀死走 /api/ams/:pkg/kill) ---- */
|
||||||
let bgTimer = null;
|
let bgTimer = null;
|
||||||
let bgData = null; // {available,count,apps:[{pkg,name,pid,state,cpu_pct,rss_kb}]}
|
let bgData = null; // {available,count,apps:[{pkg,name,pid,state,is_fg,cpu_pct,rss_kb}],fg_count,fg:[...]}
|
||||||
|
|
||||||
function stopBg() { if (bgTimer) { clearInterval(bgTimer); bgTimer = null; } }
|
function stopBg() { if (bgTimer) { clearInterval(bgTimer); bgTimer = null; } }
|
||||||
|
|
||||||
@@ -900,30 +903,43 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
return '<span class="bg-ico">' + ch + '</span>';
|
return '<span class="bg-ico">' + ch + '</span>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 状态徽标:前台=ok/绿,后台=warn/黄(复用已定义的 home-chip 组件样式)。 */
|
||||||
|
function bgStateBadge(a) {
|
||||||
|
const isFg = a.is_fg || a.state === 'fg';
|
||||||
|
return '<span class="home-chip ' + (isFg ? 'ok' : 'warn') + '">' + (isFg ? '前台' : '后台') + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 单行 App 卡(kind='fg'=前台可终止/救命;'bg'=后台可杀死)。 */
|
||||||
|
function bgRowHtml(a, kind) {
|
||||||
|
const cpu = (a.cpu_pct == null) ? '—' : a.cpu_pct.toFixed(1) + '%';
|
||||||
|
const mem = (a.rss_kb == null) ? '—' : fmtBytes(Number(a.rss_kb) * 1024);
|
||||||
|
const sub = 'CPU ' + cpu + ' · ' + mem +
|
||||||
|
(a.pid > 0 ? ' · pid ' + esc(a.pid) : '') + ' · ' + bgStateBadge(a);
|
||||||
|
const btn = (kind === 'fg')
|
||||||
|
? '<button class="btn btn-danger" data-bgkillfg="' + esc(a.pkg) + '">终止</button>'
|
||||||
|
: '<button class="btn btn-danger" data-bgkill="' + esc(a.pkg) + '">杀死</button>';
|
||||||
|
return '<div class="bg-row">' + bgIconHtml(a) +
|
||||||
|
'<div class="bg-info"><div class="bg-name">' + esc(a.name || a.pkg || '未命名') + '</div>' +
|
||||||
|
'<div class="bg-sub">' + sub + '<span class="mono">' + esc(a.pkg) + '</span></div></div>' +
|
||||||
|
btn + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
function renderBgBody(d) {
|
function renderBgBody(d) {
|
||||||
let h = '<div class="page" id="bg-page">';
|
let h = '<div class="page" id="bg-page">';
|
||||||
h += '<div class="card"><h3 class="card-title">后台活动 App(meow_amsd bg · 只读)</h3>';
|
h += '<div class="card"><h3 class="card-title">App 活动(前台可终止 · 后台管理)</h3>';
|
||||||
if (!d || d.available === false) {
|
if (!d || d.available === false) {
|
||||||
h += homeUnavailable();
|
h += homeUnavailable();
|
||||||
} else {
|
} else {
|
||||||
|
const fg = d.fg || [];
|
||||||
const apps = d.apps || [];
|
const apps = d.apps || [];
|
||||||
if (!apps.length) {
|
h += '<p class="muted">前台 App 可「终止」(救命:前台卡死/无响应时强制结束,需二次确认);后台 App 可「杀死」或一键清。</p>';
|
||||||
h += '<div class="empty">无后台 App</div>';
|
h += '<div class="bg-group">' + (fg.length ? '前台 App(' + fg.length + ')' : '前台 App') + '</div>';
|
||||||
} else {
|
if (!fg.length) h += '<div class="empty">当前无前台 App</div>';
|
||||||
h += '<div class="bg-list">';
|
else { h += '<div class="bg-list">'; fg.forEach(a => { h += bgRowHtml(a, 'fg'); }); h += '</div>'; }
|
||||||
apps.forEach(a => {
|
h += '<div class="bg-group">' + (apps.length ? '后台 App(' + apps.length + ')' : '后台 App') + '</div>';
|
||||||
const cpu = (a.cpu_pct == null) ? '—' : a.cpu_pct.toFixed(1) + '%';
|
if (!apps.length) h += '<div class="empty">无后台 App</div>';
|
||||||
const mem = (a.rss_kb == null) ? '—' : fmtBytes(Number(a.rss_kb) * 1024);
|
else { h += '<div class="bg-list">'; apps.forEach(a => { h += bgRowHtml(a, 'bg'); }); h += '</div>'; }
|
||||||
const sub = 'CPU ' + cpu + ' · ' + mem + ' · 后台' +
|
h += '<p class="muted">占用数据读自 /proc/<pid>(与启动器后台管理页同源、同口径);「终止/杀死」经 amsd(require_auth),前台终止为救命操作需二次确认。CPU 首采样为「—」,轮询后显示真实占用;数据不可得时如实显示「—」,绝不编造。</p>';
|
||||||
(a.pid > 0 ? ' · pid ' + esc(a.pid) : '');
|
|
||||||
h += '<div class="bg-row">' + bgIconHtml(a) +
|
|
||||||
'<div class="bg-info"><div class="bg-name">' + esc(a.name || a.pkg || '未命名') + '</div>' +
|
|
||||||
'<div class="bg-sub">' + sub + '<span class="mono">' + esc(a.pkg) + '</span></div></div>' +
|
|
||||||
'<button class="btn btn-danger" data-bgkill="' + esc(a.pkg) + '">杀死</button></div>';
|
|
||||||
});
|
|
||||||
h += '</div>';
|
|
||||||
}
|
|
||||||
h += '<p class="muted">占用数据读自 /proc/<pid>(与启动器后台管理页同源、同口径);「杀死」经 amsd(require_auth)。CPU 首采样为「—」,轮询后显示真实占用;数据不可得时如实显示「—」,绝不编造。</p>';
|
|
||||||
}
|
}
|
||||||
h += '</div></div>';
|
h += '</div></div>';
|
||||||
return h;
|
return h;
|
||||||
@@ -965,6 +981,15 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
fetchBgOnce();
|
fetchBgOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 前台强制终止(救命):明确标注「前台」,二次确认;后端 am kill 优先、launcher 拒则直接按进程信号。 */
|
||||||
|
async function bgKillFg(pkg) {
|
||||||
|
const ok = await confirmDlg('终止前台 App', '『' + pkg + '』当前位于前台。若它已卡死/无响应,可强制终止(救命):先请求干净结束,启动器拒绝时直接按进程信号(SIGTERM/SIGKILL)。确认终止?', true);
|
||||||
|
if (!ok) return;
|
||||||
|
try { await api('POST', '/api/ams/' + encodeURIComponent(pkg) + '/killfg'); toast('已请求终止前台 ' + pkg); }
|
||||||
|
catch (e) { toast('终止失败:' + errText(e)); }
|
||||||
|
fetchBgOnce();
|
||||||
|
}
|
||||||
|
|
||||||
async function bgClearAll() {
|
async function bgClearAll() {
|
||||||
const list = (bgData && bgData.apps) ? bgData.apps : [];
|
const list = (bgData && bgData.apps) ? bgData.apps : [];
|
||||||
if (!list.length) { toast('无后台 App 可清'); return; }
|
if (!list.length) { toast('无后台 App 可清'); return; }
|
||||||
@@ -981,6 +1006,8 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
|
|
||||||
function bindBg() {
|
function bindBg() {
|
||||||
$('#content').addEventListener('click', async function (e) {
|
$('#content').addEventListener('click', async function (e) {
|
||||||
|
const kfg = e.target.closest('[data-bgkillfg]');
|
||||||
|
if (kfg) { await bgKillFg(kfg.dataset.bgkillfg); return; }
|
||||||
const kl = e.target.closest('[data-bgkill]');
|
const kl = e.target.closest('[data-bgkill]');
|
||||||
if (kl) { await bgKill(kl.dataset.bgkill); return; }
|
if (kl) { await bgKill(kl.dataset.bgkill); return; }
|
||||||
});
|
});
|
||||||
@@ -1967,7 +1994,17 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
else if (dir === 'voldown') sendKeyRepeat(114, volSteps(ay), '音量−');
|
else if (dir === 'voldown') sendKeyRepeat(114, volSteps(ay), '音量−');
|
||||||
else if (dir === 'next') sendKeyLabel(163, '下一曲');
|
else if (dir === 'next') sendKeyLabel(163, '下一曲');
|
||||||
else if (dir === 'prev') sendKeyLabel(165, '上一曲');
|
else if (dir === 'prev') sendKeyLabel(165, '上一曲');
|
||||||
else if (dir === 'back') sendKeyLabel(158, '返回');
|
else if (dir === 'back') sendGesture('back'); // 左缘右滑 → 注入设备已识别的返回手势
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendGesture(name) {
|
||||||
|
const label = name === 'back' ? '返回' : '回桌面';
|
||||||
|
try {
|
||||||
|
const d = await api('POST', '/api/input/gesture', { gesture: name });
|
||||||
|
if (d && d.dispatched) toast(label + ' 已触发');
|
||||||
|
else if (d && d.reason === 'local_touch_active') toast('本机触摸中,注入已暂停');
|
||||||
|
else toast(label + ' 触发未派发');
|
||||||
|
} catch (e) { toast(label + ' 触发失败:' + errText(e)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function remotePointerDown(e) {
|
function remotePointerDown(e) {
|
||||||
@@ -2010,6 +2047,8 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
h += remoteBtn('next', 'next', '下一曲', '下一曲(code=163)');
|
h += remoteBtn('next', 'next', '下一曲', '下一曲(code=163)');
|
||||||
h += remoteBtn('prev', 'prev', '上一曲', '上一曲(code=165)');
|
h += remoteBtn('prev', 'prev', '上一曲', '上一曲(code=165)');
|
||||||
h += remoteBtn('power', 'power', '电源', '电源键(单击;code=116)');
|
h += remoteBtn('power', 'power', '电源', '电源键(单击;code=116)');
|
||||||
|
h += remoteBtn('back', 'back', '返回', '左缘右滑返回(注入设备已识别的返回手势)');
|
||||||
|
h += remoteBtn('home', 'home', '回桌面', '底部上滑回桌面(注入设备已识别的回桌面手势)');
|
||||||
h += '</div>';
|
h += '</div>';
|
||||||
h += '<div class="remote-bright">' +
|
h += '<div class="remote-bright">' +
|
||||||
'<span class="r-lbl">指定亮度</span>' +
|
'<span class="r-lbl">指定亮度</span>' +
|
||||||
@@ -2018,7 +2057,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
'</div>';
|
'</div>';
|
||||||
h += '<button class="btn" id="ctl-powerlong">' + (MP_ICONS.power || '') + ' <span>长按电源(电源菜单)</span></button>';
|
h += '<button class="btn" id="ctl-powerlong">' + (MP_ICONS.power || '') + ' <span>长按电源(电源菜单)</span></button>';
|
||||||
h += '<p class="muted" style="margin-top:8px;line-height:1.8">滑动手势(在遥控板上):上滑=音量+ · 下滑=音量- · 左滑=下一曲 · 右滑=上一曲 · 左缘右滑=返回。<br>' +
|
h += '<p class="muted" style="margin-top:8px;line-height:1.8">滑动手势(在遥控板上):上滑=音量+ · 下滑=音量- · 左滑=下一曲 · 右滑=上一曲 · 左缘右滑=返回。<br>' +
|
||||||
'<b>返回/主页</b>属物理键红线(<code>/api/key</code> 后端禁注入 158/172),故未提供注入按钮。</p>';
|
'<b>返回/回桌面</b>走手势注入(<code>/api/input/gesture</code>,模拟设备已识别的左缘右滑/底部上滑),<b>不走</b>被物理键红线禁注入的 <code>/api/key</code> 158/172。</p>';
|
||||||
h += '</div>';
|
h += '</div>';
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
@@ -2484,6 +2523,8 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
|||||||
else if (a === 'next') sendKey(163, 'tap');
|
else if (a === 'next') sendKey(163, 'tap');
|
||||||
else if (a === 'prev') sendKey(165, 'tap');
|
else if (a === 'prev') sendKey(165, 'tap');
|
||||||
else if (a === 'power') sendKey(116, 'tap');
|
else if (a === 'power') sendKey(116, 'tap');
|
||||||
|
else if (a === 'back') sendGesture('back');
|
||||||
|
else if (a === 'home') sendGesture('home');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.target.closest('#ctl-powerlong')) { sendKey(116, 'long', 3000); return; }
|
if (e.target.closest('#ctl-powerlong')) { sendKey(116, 'long', 3000); return; }
|
||||||
|
|||||||
@@ -8,12 +8,14 @@
|
|||||||
#include "book.h"
|
#include "book.h"
|
||||||
#include "rdsrc.h"
|
#include "rdsrc.h"
|
||||||
#include "rdtxt.h"
|
#include "rdtxt.h"
|
||||||
|
#include "seg.h"
|
||||||
#include "rdlayout.h"
|
#include "rdlayout.h"
|
||||||
#include "rdimg.h"
|
#include "rdimg.h"
|
||||||
#include "../engine/rdft.h"
|
#include "../engine/rdft.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
typedef struct seg {
|
typedef struct seg {
|
||||||
int page_start; /* 全局页表起点 */
|
int page_start; /* 全局页表起点 */
|
||||||
@@ -43,8 +45,59 @@ struct rd_book {
|
|||||||
uint32_t *seg_cps; size_t seg_cps_n, seg_cps_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;
|
rd_line *seg_lines; rd_page *seg_pages; int seg_nlines, seg_npages;
|
||||||
int seg_cache_idx;
|
int seg_cache_idx;
|
||||||
|
|
||||||
|
/* 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 路径增量增长) */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ---- 页索引持久化(MeowData;见 rd_book_set_index_dir) ----
|
||||||
|
* build_index 对整书逐段 reflow 建 flat 页表,是开大书最贵的步骤(无持久化时每次打开都重跑)。
|
||||||
|
* 宿主快 ≠ 真机快:同一书 MIPS 上仍需数秒。这里把 (segs + pages) 落盘成 {path 哈希}.idx,
|
||||||
|
* 以 (file_size, file_mtime, font path+mt, fs, ls, content_w, content_h, enc) 为失效键——
|
||||||
|
* 书/字号/行距/版面/编码/字体任一变了即自动重排并重写,其余情况命中缓存秒开。
|
||||||
|
* 纯 libc(无 SDK):reader.c 用 meow_config_dir 推导 MeowData dir 注入;冒烟/无卡时 dir 为空 → 直排不持久。 */
|
||||||
|
|
||||||
|
static char g_index_dir[400]; /* 索引缓存目录; 空 = 禁用 */
|
||||||
|
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; }
|
||||||
|
else g_index_dir[0] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* R3.2:分段流式缓存根(MeowData/os.meow.reader)。存到 g_seg_base 并同步 rdseg。 */
|
||||||
|
static char g_seg_base[400];
|
||||||
|
void rd_book_set_seg_base(const char *base)
|
||||||
|
{
|
||||||
|
if (base && base[0]) { strncpy(g_seg_base, base, sizeof g_seg_base - 1); g_seg_base[sizeof g_seg_base - 1] = 0; }
|
||||||
|
else g_seg_base[0] = 0;
|
||||||
|
rdseg_set_base(g_seg_base);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define IDX_MAGIC0 'R'
|
||||||
|
#define IDX_MAGIC1 'I'
|
||||||
|
#define IDX_MAGIC2 'D'
|
||||||
|
#define IDX_MAGIC3 'X'
|
||||||
|
#define IDX_VER 1u
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
static uint32_t idx_fnv1a(const char *s)
|
||||||
|
{
|
||||||
|
uint32_t h = 2166136261u;
|
||||||
|
for (const unsigned char *p = (const unsigned char *)s; *p; p++) { h ^= *p; h *= 16777619u; }
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
static int index_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.idx", g_index_dir, idx_fnv1a(b->path)) >= outsz) return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* 编码枚举 → 可读名(R3:Big5 正确解码后名实相符)。 */
|
/* 编码枚举 → 可读名(R3:Big5 正确解码后名实相符)。 */
|
||||||
static const char *enc_to_name(rd_enc_t enc)
|
static const char *enc_to_name(rd_enc_t enc)
|
||||||
{
|
{
|
||||||
@@ -71,6 +124,135 @@ static int book_meas(uint32_t cp, int fs, int *adv, void *ctx)
|
|||||||
return rdft_advance(b->ft, cp, adv);
|
return rdft_advance(b->ft, cp, adv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 失效键:书 + 版面 + 编码 + 字体。任一变化 → 缓存不命中(重排)。 */
|
||||||
|
typedef struct {
|
||||||
|
uint64_t file_size;
|
||||||
|
int64_t file_mtime;
|
||||||
|
int64_t font_mtime; /* 字体文件 mtime;无字体(近似测宽) = 0 */
|
||||||
|
int32_t fs, ls, cw, ch, enc;
|
||||||
|
char font[320];
|
||||||
|
} idx_key;
|
||||||
|
|
||||||
|
static int index_key_fill(rd_book *b, idx_key *k)
|
||||||
|
{
|
||||||
|
memset(k, 0, sizeof *k);
|
||||||
|
struct stat st;
|
||||||
|
if (stat(b->path, &st) != 0) { k->file_size = 0; k->file_mtime = 0; }
|
||||||
|
else { k->file_size = (uint64_t)st.st_size; k->file_mtime = (int64_t)st.st_mtime; }
|
||||||
|
if (b->font[0]) {
|
||||||
|
/* b->font[400] 已 NUL 结尾(open 时 <400 才收);memcpy 319B 恒在界内且 k 已清零 → 余下为 0 */
|
||||||
|
memcpy(k->font, b->font, sizeof k->font - 1);
|
||||||
|
struct stat fs2;
|
||||||
|
if (stat(b->font, &fs2) == 0) k->font_mtime = (int64_t)fs2.st_mtime;
|
||||||
|
}
|
||||||
|
k->fs = b->fs; k->ls = b->ls; k->cw = b->content_w; k->ch = b->content_h;
|
||||||
|
k->enc = (int32_t)b->enc;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int index_key_write(const idx_key *k, FILE *f)
|
||||||
|
{
|
||||||
|
const uint8_t magic[4] = { IDX_MAGIC0, IDX_MAGIC1, IDX_MAGIC2, IDX_MAGIC3 };
|
||||||
|
uint16_t ver = IDX_VER;
|
||||||
|
return fio_wr(f, magic, sizeof magic) && fio_wr(f, &ver, sizeof ver) &&
|
||||||
|
fio_wr(f, &k->file_size, sizeof k->file_size) &&
|
||||||
|
fio_wr(f, &k->file_mtime, sizeof k->file_mtime) && fio_wr(f, &k->font_mtime, sizeof k->font_mtime) &&
|
||||||
|
fio_wr(f, &k->fs, sizeof k->fs) && fio_wr(f, &k->ls, sizeof k->ls) &&
|
||||||
|
fio_wr(f, &k->cw, sizeof k->cw) && fio_wr(f, &k->ch, sizeof k->ch) &&
|
||||||
|
fio_wr(f, &k->enc, sizeof k->enc) && fio_wr(f, k->font, sizeof k->font);
|
||||||
|
}
|
||||||
|
static int index_key_read(idx_key *k, FILE *f)
|
||||||
|
{
|
||||||
|
uint8_t magic[4];
|
||||||
|
if (!fio_rd(f, magic, sizeof magic)) return 0;
|
||||||
|
if (magic[0] != IDX_MAGIC0 || magic[1] != IDX_MAGIC1 || magic[2] != IDX_MAGIC2 || magic[3] != IDX_MAGIC3) return 0;
|
||||||
|
uint16_t ver = 0;
|
||||||
|
if (!fio_rd(f, &ver, sizeof ver) || ver != IDX_VER) return 0;
|
||||||
|
if (!fio_rd(f, &k->file_size, sizeof k->file_size) || !fio_rd(f, &k->file_mtime, sizeof k->file_mtime) ||
|
||||||
|
!fio_rd(f, &k->font_mtime, sizeof k->font_mtime) || !fio_rd(f, &k->fs, sizeof k->fs) ||
|
||||||
|
!fio_rd(f, &k->ls, sizeof k->ls) || !fio_rd(f, &k->cw, sizeof k->cw) ||
|
||||||
|
!fio_rd(f, &k->ch, sizeof k->ch) || !fio_rd(f, &k->enc, sizeof k->enc) ||
|
||||||
|
!fio_rd(f, k->font, sizeof k->font)) return 0;
|
||||||
|
k->font[sizeof k->font - 1] = 0;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
static int index_key_eq(const idx_key *a, const idx_key *b)
|
||||||
|
{
|
||||||
|
return a->file_size == b->file_size && a->file_mtime == b->file_mtime &&
|
||||||
|
a->font_mtime == b->font_mtime && a->fs == b->fs && a->ls == b->ls &&
|
||||||
|
a->cw == b->cw && a->ch == b->ch && a->enc == b->enc &&
|
||||||
|
strncmp(a->font, b->font, sizeof a->font) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 读缓存:头部与当前键一致则载入 (segs,pages) 并自检一致;返回 1=命中,0=未命中/损坏(调用方重排)。 */
|
||||||
|
static int index_cache_load(rd_book *b)
|
||||||
|
{
|
||||||
|
char path[440];
|
||||||
|
if (index_cache_path(b, path, sizeof path) != 0) return 0;
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) return 0;
|
||||||
|
idx_key key, cur;
|
||||||
|
int rc = 0;
|
||||||
|
if (index_key_read(&key, f)) {
|
||||||
|
index_key_fill(b, &cur);
|
||||||
|
if (index_key_eq(&key, &cur)) {
|
||||||
|
int32_t nsegs = 0, npages = 0;
|
||||||
|
if (fio_rd(f, &nsegs, sizeof nsegs) && fio_rd(f, &npages, sizeof npages) &&
|
||||||
|
nsegs >= 1 && npages >= 1 && nsegs <= 1000000 && npages <= 8000000 &&
|
||||||
|
(uint64_t)npages <= cur.file_size + 1 && (uint64_t)nsegs <= cur.file_size + 1) {
|
||||||
|
seg_t *segs = (seg_t *)malloc((size_t)nsegs * sizeof(seg_t));
|
||||||
|
rd_book_page *pages = (rd_book_page *)malloc((size_t)npages * sizeof(rd_book_page));
|
||||||
|
if (segs && pages &&
|
||||||
|
fio_rd(f, segs, (size_t)nsegs * sizeof(seg_t)) &&
|
||||||
|
fio_rd(f, pages, (size_t)npages * sizeof(rd_book_page))) {
|
||||||
|
/* 自检:page_count 求和 == npages 且 page_start 链正确;每页 seg 落在所属段区间。 */
|
||||||
|
int ok = 1, run = 0;
|
||||||
|
for (int32_t i = 0; i < nsegs && ok; i++) {
|
||||||
|
if (segs[i].page_count < 0 || segs[i].page_start != run) ok = 0;
|
||||||
|
else run += segs[i].page_count;
|
||||||
|
}
|
||||||
|
if (ok && run == npages) {
|
||||||
|
for (int32_t p = 0; p < npages && ok; p++) {
|
||||||
|
int32_t sg = pages[p].seg;
|
||||||
|
if (sg < 0 || sg >= nsegs || pages[p].seg_char < 0 ||
|
||||||
|
p < segs[sg].page_start || p >= segs[sg].page_start + segs[sg].page_count) ok = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
b->segs = segs; b->pages = pages;
|
||||||
|
b->nsegs = (int)nsegs; b->npages = (int)npages;
|
||||||
|
rc = 1; /* 命中:调用方免 build_index */
|
||||||
|
} else { free(segs); free(pages); }
|
||||||
|
} else { free(segs); free(pages); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 写缓存:先写 .tmp 再 rename(原子替换);失败静默(下次重排)。 */
|
||||||
|
static void index_cache_save(rd_book *b)
|
||||||
|
{
|
||||||
|
char path[440];
|
||||||
|
if (b->nsegs <= 0 || b->npages <= 0) return; /* 空书无页表可缓存 */
|
||||||
|
if (index_cache_path(b, path, sizeof path) != 0) return;
|
||||||
|
/* 目录由 reader.c 在 set_index_dir 前建好;这里 fopen 失败(无目录/只读)→ 静默跳过(下次重排)。 */
|
||||||
|
char tmp[480];
|
||||||
|
if (snprintf(tmp, sizeof tmp, "%s.tmp", path) >= (int)sizeof tmp) return;
|
||||||
|
FILE *f = fopen(tmp, "wb");
|
||||||
|
if (!f) return;
|
||||||
|
idx_key key;
|
||||||
|
int ok = 1;
|
||||||
|
index_key_fill(b, &key);
|
||||||
|
ok = ok && index_key_write(&key, f);
|
||||||
|
ok = ok && fio_wr(f, &b->nsegs, sizeof b->nsegs) && fio_wr(f, &b->npages, sizeof b->npages);
|
||||||
|
if (ok && b->nsegs > 0) ok = fio_wr(f, b->segs, (size_t)b->nsegs * sizeof(seg_t));
|
||||||
|
if (ok && b->npages > 0) ok = fio_wr(f, b->pages, (size_t)b->npages * sizeof(rd_book_page));
|
||||||
|
fclose(f);
|
||||||
|
if (ok) { rename(tmp, path); } else { remove(tmp); }
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- 文本:建索引(逐段 layout) ---- */
|
/* ---- 文本:建索引(逐段 layout) ---- */
|
||||||
static int build_index(rd_book *b)
|
static int build_index(rd_book *b)
|
||||||
{
|
{
|
||||||
@@ -118,15 +300,117 @@ static int build_index(rd_book *b)
|
|||||||
return 0;
|
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)
|
||||||
|
{
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
free(lines); free(pages);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- R3.2 段路径:把 [seg_built, upto_seg] 逐段(512KB 段文件 → utf8 → 布局)补齐进 flat 页表。
|
||||||
|
* 幂等:已建段跳过。开书只建到目标(页 0/保存进度),翻页/跳转再按需补齐 —— 免整书一次性布局。 */
|
||||||
|
static int seg_ensure_built(rd_book *b, int upto_seg)
|
||||||
|
{
|
||||||
|
if (!b->seg_dir[0] || b->fmt != RD_FMT_TXT) return 0;
|
||||||
|
int total = b->seg_n > 0 ? b->seg_n : 1;
|
||||||
|
if (upto_seg >= total) upto_seg = total - 1;
|
||||||
|
if (upto_seg < 0) upto_seg = 0;
|
||||||
|
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;
|
||||||
|
seg_t *ns = (seg_t *)realloc(b->segs, (size_t)nc * sizeof(seg_t));
|
||||||
|
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;
|
||||||
|
b->seg_built++;
|
||||||
|
built_any = 1;
|
||||||
|
}
|
||||||
|
b->nsegs = b->seg_built;
|
||||||
|
if (built_any) index_cache_save(b);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- 正文重显 + 布局到复用缓冲 ---- */
|
/* ---- 正文重显 + 布局到复用缓冲 ---- */
|
||||||
static int load_seg(rd_book *b, int seg_idx)
|
static int load_seg(rd_book *b, int seg_idx)
|
||||||
{
|
{
|
||||||
if (seg_idx == b->seg_cache_idx) return 0;
|
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;
|
size_t cap = RDTXT_SEG_UTF8_MAX + 8;
|
||||||
if (!b->seg_utf8) b->seg_utf8 = (uint8_t *)malloc(cap);
|
if (!b->seg_utf8) b->seg_utf8 = (uint8_t *)malloc(cap);
|
||||||
if (!b->seg_utf8) return -6;
|
if (!b->seg_utf8) return -6;
|
||||||
size_t ulen = 0;
|
|
||||||
if (rd_src_seg(b->src, seg_idx, b->seg_utf8, cap, &ulen) != 0) return -4;
|
if (rd_src_seg(b->src, seg_idx, b->seg_utf8, cap, &ulen) != 0) return -4;
|
||||||
|
}
|
||||||
size_t n = 0;
|
size_t n = 0;
|
||||||
free(b->seg_cps); b->seg_cps = NULL;
|
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;
|
if (rd_utf8_to_cp(b->seg_utf8, ulen, &b->seg_cps, &n) != RD_ENC_OK) return -5;
|
||||||
@@ -182,7 +466,36 @@ int rd_book_open(rd_book **b, const char *path, const char *font_path,
|
|||||||
if (font_path && rdft_load_font(x->ft, x->font) != 0) { rdft_free(x->ft); x->ft = NULL; 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);
|
if (x->ft) rdft_set_px(x->ft, fs);
|
||||||
x->seg_cache_idx = -1;
|
x->seg_cache_idx = -1;
|
||||||
|
int seg_mode = 0;
|
||||||
|
/* R3.2 分段流式(TXT 且段 base 已配置):开书切 512KB 段文件,逐段按需布局(不全量)。 */
|
||||||
|
if (g_seg_base[0] && x->fmt == RD_FMT_TXT) {
|
||||||
|
char sdir[600];
|
||||||
|
if (rdseg_dir_for(x->path, sdir, sizeof sdir) == 0) {
|
||||||
|
strncpy(x->seg_dir, sdir, sizeof x->seg_dir - 1); x->seg_dir[sizeof x->seg_dir - 1] = 0;
|
||||||
|
int ns = rdseg_build(x->path, x->enc, x->seg_dir);
|
||||||
|
if (ns > 0) { x->seg_n = ns; seg_mode = 1; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (seg_mode) {
|
||||||
|
/* 命中缓存(书+版面+编码+字体未变)→ 载入已建 (segs,pages);否则仅布局段 0 起步,
|
||||||
|
* 翻页/跳转再 seg_ensure_built 补齐(治大书开书整表布局卡 UI 线程)。 */
|
||||||
|
x->segs = NULL; x->pages = NULL; x->nsegs = x->npages = 0;
|
||||||
|
x->seg_built = 0; x->segs_cap = x->pages_cap = 0;
|
||||||
|
if (index_cache_load(x) != 1) {
|
||||||
|
if (seg_ensure_built(x, 0) != 0) goto close;
|
||||||
|
}
|
||||||
|
if (x->nsegs < 1) { /* 段 0 空(理论)→ 兜底再建 */
|
||||||
|
if (seg_ensure_built(x, 0) != 0) goto close;
|
||||||
|
}
|
||||||
|
x->segs_cap = x->nsegs; x->pages_cap = x->npages;
|
||||||
|
x->seg_built = x->nsegs; /* 缓存命中时恢复到已建段数 */
|
||||||
|
} else {
|
||||||
|
/* 页索引缓存命中(书+字号+版面+编码+字体未变)→ 免整书重排,秒开;未命中 → 重排并落盘。 */
|
||||||
|
if (index_cache_load(x) != 1) {
|
||||||
if (build_index(x) != 0) goto close;
|
if (build_index(x) != 0) goto close;
|
||||||
|
index_cache_save(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
int pages = rd_src_pages(x->src);
|
int pages = rd_src_pages(x->src);
|
||||||
x->npages = pages > 0 ? pages : 0;
|
x->npages = pages > 0 ? pages : 0;
|
||||||
@@ -240,6 +553,7 @@ int rd_book_set_enc(rd_book *b, rd_enc_t enc)
|
|||||||
int seg; int32_t ch;
|
int seg; int32_t ch;
|
||||||
rd_book_anchor(b, &seg, &ch);
|
rd_book_anchor(b, &seg, &ch);
|
||||||
|
|
||||||
|
rd_enc_t oldenc = b->enc;
|
||||||
rd_src *ns = NULL;
|
rd_src *ns = NULL;
|
||||||
if (rd_src_open_with_enc(&ns, b->path, &enc) != 0) return -2; /* 旧 src 未动 */
|
if (rd_src_open_with_enc(&ns, b->path, &enc) != 0) return -2; /* 旧 src 未动 */
|
||||||
rd_src_free(b->src); b->src = ns; /* 换新 */
|
rd_src_free(b->src); b->src = ns; /* 换新 */
|
||||||
@@ -248,8 +562,17 @@ int rd_book_set_enc(rd_book *b, rd_enc_t enc)
|
|||||||
|
|
||||||
/* 重建索引(复用现值 fs/ls/content_*),再回锚 */
|
/* 重建索引(复用现值 fs/ls/content_*),再回锚 */
|
||||||
free(b->segs); free(b->pages); b->segs = NULL; b->pages = NULL;
|
free(b->segs); free(b->pages); b->segs = NULL; b->pages = NULL;
|
||||||
b->nsegs = b->npages = 0;
|
b->nsegs = b->npages = 0; b->seg_built = 0; b->segs_cap = b->pages_cap = 0;
|
||||||
|
if (b->seg_dir[0]) {
|
||||||
|
/* R3.2 段路径:编码变了 → 段文件边界/解码不同 → 重建段文件 + 按需布局(段 0 起步) */
|
||||||
|
if (oldenc != b->enc) rdseg_clear(b->seg_dir);
|
||||||
|
b->seg_n = rdseg_build(b->path, b->enc, b->seg_dir);
|
||||||
|
if (seg_ensure_built(b, 0) != 0) return -3;
|
||||||
|
index_cache_save(b);
|
||||||
|
} else {
|
||||||
if (build_index(b) != 0) return -3; /* 新 src 有效但索引空 → 渲染返回 -1,安全降级 */
|
if (build_index(b) != 0) return -3; /* 新 src 有效但索引空 → 渲染返回 -1,安全降级 */
|
||||||
|
index_cache_save(b); /* 编码切换后重新落盘(键含 enc,与旧缓存并存不冲突) */
|
||||||
|
}
|
||||||
return rd_book_jump_to_anchor(b, seg, ch, b->fs, b->ls);
|
return rd_book_jump_to_anchor(b, seg, ch, b->fs, b->ls);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,6 +589,12 @@ int rd_book_turn(rd_book *b, int delta)
|
|||||||
if (!b || b->npages <= 0) return 0;
|
if (!b || b->npages <= 0) return 0;
|
||||||
int np = b->cur + delta;
|
int np = b->cur + delta;
|
||||||
if (np < 0) np = 0;
|
if (np < 0) np = 0;
|
||||||
|
/* R3.2 段路径:翻到已建表尾之外 → 按需补下一段(一次翻页只补到够用,不全量) */
|
||||||
|
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT && np >= b->npages && b->seg_built < b->seg_n) {
|
||||||
|
while (np >= b->npages && b->seg_built < b->seg_n) {
|
||||||
|
if (seg_ensure_built(b, b->seg_built) != 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (np >= b->npages) np = b->npages - 1;
|
if (np >= b->npages) np = b->npages - 1;
|
||||||
b->cur = np;
|
b->cur = np;
|
||||||
return np;
|
return np;
|
||||||
@@ -274,6 +603,12 @@ int rd_book_turn(rd_book *b, int delta)
|
|||||||
int rd_book_set_page(rd_book *b, int page)
|
int rd_book_set_page(rd_book *b, int page)
|
||||||
{
|
{
|
||||||
if (!b || b->npages <= 0) return 0;
|
if (!b || b->npages <= 0) return 0;
|
||||||
|
/* R3.2 段路径:跳到已建表外 → 按需补段到目标页(跳到哪建到哪) */
|
||||||
|
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT && page >= b->npages && b->seg_built < b->seg_n) {
|
||||||
|
while (page >= b->npages && b->seg_built < b->seg_n) {
|
||||||
|
if (seg_ensure_built(b, b->seg_built) != 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (page < 0) page = 0;
|
if (page < 0) page = 0;
|
||||||
if (page >= b->npages) page = b->npages - 1;
|
if (page >= b->npages) page = b->npages - 1;
|
||||||
b->cur = page;
|
b->cur = page;
|
||||||
@@ -372,8 +707,20 @@ int rd_book_jump_to_anchor(rd_book *b, int seg, int32_t ch, int fs, int ls)
|
|||||||
seg_t *nos = b->segs; int n_oldsegs = b->nsegs;
|
seg_t *nos = b->segs; int n_oldsegs = b->nsegs;
|
||||||
rd_book_page *nop = b->pages; int n_oldpages = b->npages;
|
rd_book_page *nop = b->pages; int n_oldpages = b->npages;
|
||||||
b->segs = NULL; b->pages = NULL; b->nsegs = b->npages = 0;
|
b->segs = NULL; b->pages = NULL; b->nsegs = b->npages = 0;
|
||||||
|
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT) {
|
||||||
|
/* R3.2 段路径:只重布局到含锚的段(不全量;段文件不变,仅页表变) */
|
||||||
|
b->seg_built = 0; b->segs_cap = b->pages_cap = 0;
|
||||||
|
if (seg_ensure_built(b, seg) != 0) { b->segs = nos; b->pages = nop; b->nsegs = n_oldsegs; b->npages = n_oldpages; return -5; }
|
||||||
|
free(nos); free(nop);
|
||||||
|
index_cache_save(b);
|
||||||
|
} else {
|
||||||
if (build_index(b) != 0) { b->segs = nos; b->pages = nop; b->nsegs = n_oldsegs; b->npages = n_oldpages; return -5; }
|
if (build_index(b) != 0) { b->segs = nos; b->pages = nop; b->nsegs = n_oldsegs; b->npages = n_oldpages; return -5; }
|
||||||
free(nos); free(nop);
|
free(nos); free(nop);
|
||||||
|
index_cache_save(b); /* 字号/行距改变后重新落盘(键含 fs/ls) */
|
||||||
|
}
|
||||||
|
} else if (b->seg_dir[0] && b->fmt == RD_FMT_TXT && seg >= b->seg_built) {
|
||||||
|
/* 锚在未建段 → 按需补齐(重开续读/目录跳转到后部) */
|
||||||
|
if (seg_ensure_built(b, seg) != 0) return -5;
|
||||||
}
|
}
|
||||||
int target = 0;
|
int target = 0;
|
||||||
for (int p = 0; p < b->npages; p++) {
|
for (int p = 0; p < b->npages; p++) {
|
||||||
|
|||||||
@@ -64,6 +64,15 @@ const char *rd_book_path(const rd_book *b); /* 原始路径 */
|
|||||||
* 用于「编码」菜单(Big5 繁体书手动选定)。 */
|
* 用于「编码」菜单(Big5 繁体书手动选定)。 */
|
||||||
int rd_book_set_enc(rd_book *b, rd_enc_t enc);
|
int rd_book_set_enc(rd_book *b, rd_enc_t enc);
|
||||||
|
|
||||||
|
/* R3.1:页索引持久化缓存目录(MeowData/os.meow.reader/index)。dir 为空 → 禁持久化(直排,不落盘)。
|
||||||
|
* 由 reader.c 在 open 前建目录并注入;书/字号/行距/版面/编码/字体任一变化时缓存自动失效重排。 */
|
||||||
|
void rd_book_set_index_dir(const char *dir);
|
||||||
|
|
||||||
|
/* R3.2:TXT 分段流式缓存根(MeowData/os.meow.reader,与 index 同父)。base 为空 → 禁分段(退回全量直排)。
|
||||||
|
* 由 reader.c 在 open 前注入;book.c 据此派生 <base>/seg/<path哈希> 段目录,开书时把 txt 切 512KB
|
||||||
|
* 码位对齐段文件,渲染只加载当前页所在段(不全量读整书)——治大书开书 CPU100%/返回无效。 */
|
||||||
|
void rd_book_set_seg_base(const char *base);
|
||||||
|
|
||||||
/* 字体/几何 */
|
/* 字体/几何 */
|
||||||
int rd_book_font(rd_book *b); /* 当前字号 */
|
int rd_book_font(rd_book *b); /* 当前字号 */
|
||||||
int rd_book_ls(rd_book *b); /* 行距% */
|
int rd_book_ls(rd_book *b); /* 行距% */
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ static uint32_t g_page_bg, g_page_fg; /* 正文页底/字色(24bit) */
|
|||||||
static char g_chapter[128]; /* 顶栏章节名(TXT 无章节=文件名) */
|
static char g_chapter[128]; /* 顶栏章节名(TXT 无章节=文件名) */
|
||||||
static int g_show_header, g_show_footer; /* 页头条/底条显隐(rdconf.rd.header/footer) */
|
static int g_show_header, g_show_footer; /* 页头条/底条显隐(rdconf.rd.header/footer) */
|
||||||
static volatile int g_volpage_pending; /* 系统翻页信号待消费(IPC 线程置位,UI tick 消费,E) */
|
static volatile int g_volpage_pending; /* 系统翻页信号待消费(IPC 线程置位,UI tick 消费,E) */
|
||||||
|
static int g_prog_dirty; /* 阅读进度变更待写(翻页/刮擦/字号/编码;R3.2 定时落盘防崩溃丢进度) */
|
||||||
|
static uint64_t g_prog_last_save_ms; /* 上次进度落盘时刻(tick 节流) */
|
||||||
|
|
||||||
#define RD_READER_DEFAULT_FS 20
|
#define RD_READER_DEFAULT_FS 20
|
||||||
#define RD_READER_DEFAULT_LS 130
|
#define RD_READER_DEFAULT_LS 130
|
||||||
@@ -176,6 +178,7 @@ static void reader_reflow(void)
|
|||||||
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } /* 重排/雷字号时终止翻页动画 */
|
if (g_anim) { meow_anim_cancel(g_anim); g_anim = NULL; } /* 重排/雷字号时终止翻页动画 */
|
||||||
int seg = 0; int32_t ch = 0;
|
int seg = 0; int32_t ch = 0;
|
||||||
rd_book_anchor(g_book, &seg, &ch); /* 记录当前锚,重排后回锚 */
|
rd_book_anchor(g_book, &seg, &ch); /* 记录当前锚,重排后回锚 */
|
||||||
|
g_prog_dirty = 1;
|
||||||
rd_book_jump_to_anchor(g_book, seg, ch, g_fs, g_ls);
|
rd_book_jump_to_anchor(g_book, seg, ch, g_fs, g_ls);
|
||||||
reader_refill_page();
|
reader_refill_page();
|
||||||
reader_redraw();
|
reader_redraw();
|
||||||
@@ -192,6 +195,7 @@ static void reader_reflow_persist(void)
|
|||||||
p.enc = (g_book && !rd_book_is_image(g_book)) ? (int)g_enc_cycle[g_enc_idx] : -1;
|
p.enc = (g_book && !rd_book_is_image(g_book)) ? (int)g_enc_cycle[g_enc_idx] : -1;
|
||||||
p.ts = (uint64_t)time(NULL);
|
p.ts = (uint64_t)time(NULL);
|
||||||
rdprog_save(rd_book_path(g_book), &p);
|
rdprog_save(rd_book_path(g_book), &p);
|
||||||
|
g_prog_dirty = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 渲染(W_CUSTOM render_cb) ---------- */
|
/* ---------- 渲染(W_CUSTOM render_cb) ---------- */
|
||||||
@@ -562,13 +566,15 @@ static void reader_turn_animated(int dir)
|
|||||||
/* 无页位图(atlas 兜底):瞬切 */
|
/* 无页位图(atlas 兜底):瞬切 */
|
||||||
if (!(g_has_font || rd_book_is_image(g_book)) || !g_page_buf[RD_PAGE_SLOT_CUR]) {
|
if (!(g_has_font || rd_book_is_image(g_book)) || !g_page_buf[RD_PAGE_SLOT_CUR]) {
|
||||||
rd_book_turn(g_book, dir);
|
rd_book_turn(g_book, dir);
|
||||||
|
g_prog_dirty = 1;
|
||||||
reader_refill_page(); reader_redraw(); reader_reflow_persist();
|
reader_refill_page(); reader_redraw(); reader_reflow_persist();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!g_anim_old) g_anim_old = (uint16_t *)malloc((size_t)g_pw * g_ph * 2);
|
if (!g_anim_old) g_anim_old = (uint16_t *)malloc((size_t)g_pw * g_ph * 2);
|
||||||
if (!g_anim_old) { rd_book_turn(g_book, dir); reader_refill_page(); reader_redraw(); return; }
|
if (!g_anim_old) { rd_book_turn(g_book, dir); g_prog_dirty = 1; reader_refill_page(); reader_redraw(); return; }
|
||||||
memcpy(g_anim_old, g_page_buf[RD_PAGE_SLOT_CUR], (size_t)g_pw * g_ph * 2);
|
memcpy(g_anim_old, g_page_buf[RD_PAGE_SLOT_CUR], (size_t)g_pw * g_ph * 2);
|
||||||
rd_book_turn(g_book, dir);
|
rd_book_turn(g_book, dir);
|
||||||
|
g_prog_dirty = 1;
|
||||||
reader_refill_page();
|
reader_refill_page();
|
||||||
g_anim_dir = dir;
|
g_anim_dir = dir;
|
||||||
meow_anim_desc d = { 0.0f, 1.0f, 140, MEOW_EASE_OUT, reader_anim_update, reader_anim_done, NULL };
|
meow_anim_desc d = { 0.0f, 1.0f, 140, MEOW_EASE_OUT, reader_anim_update, reader_anim_done, NULL };
|
||||||
@@ -591,6 +597,7 @@ static void reader_scrub_jump(int page)
|
|||||||
int np = rd_book_pages(g_book);
|
int np = rd_book_pages(g_book);
|
||||||
if (np <= 0) return;
|
if (np <= 0) return;
|
||||||
rd_book_set_page(g_book, page);
|
rd_book_set_page(g_book, page);
|
||||||
|
g_prog_dirty = 1;
|
||||||
reader_refill_page();
|
reader_refill_page();
|
||||||
reader_redraw(); /* 整页落定:正文 + 顶栏 + 刮擦条 */
|
reader_redraw(); /* 整页落定:正文 + 顶栏 + 刮擦条 */
|
||||||
reader_reflow_persist();
|
reader_reflow_persist();
|
||||||
@@ -633,6 +640,30 @@ static void reader_scrub_begin(int x, int y)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 打开 ---------- */
|
/* ---------- 打开 ---------- */
|
||||||
|
/* R3.1:页索引缓存目录 = <TF>/MeowData/os.meow.reader/index。由 meow_config_dir
|
||||||
|
* (<TF>/MeowData/<pkg>/data) 剥尾 "/data" → <TF>/MeowData/<pkg>,接 "index",mkdir -p 后注入。
|
||||||
|
* 同基座派生 R3.2 分段流式缓存根 <TF>/MeowData/os.meow.reader(book.c 再接 /seg/<hash>)。 */
|
||||||
|
static void reader_setup_index_dir(void)
|
||||||
|
{
|
||||||
|
const char *data = meow_config_dir();
|
||||||
|
if (!data || !data[0]) return; /* 无卡/宿主:不持久(book.c 直排) */
|
||||||
|
char base[420];
|
||||||
|
size_t n = strlen(data);
|
||||||
|
const char *suf = "/data"; size_t sl = strlen(suf);
|
||||||
|
size_t keep = (n >= sl && strcmp(data + n - sl, suf) == 0) ? n - sl : n;
|
||||||
|
if (keep >= sizeof base) keep = sizeof base - 1;
|
||||||
|
memcpy(base, data, keep); base[keep] = 0;
|
||||||
|
char dir[440];
|
||||||
|
if (snprintf(dir, sizeof dir, "%s/index", base) >= (int)sizeof dir) return;
|
||||||
|
/* mkdir -p 幂等(progress 同款) */
|
||||||
|
char t[440];
|
||||||
|
strncpy(t, dir, sizeof t - 1); t[sizeof t - 1] = 0;
|
||||||
|
for (char *c = t + 1; *c; c++) if (*c == '/') { *c = 0; mkdir(t, 0755); *c = '/'; }
|
||||||
|
mkdir(t, 0755);
|
||||||
|
rd_book_set_index_dir(dir);
|
||||||
|
rd_book_set_seg_base(base); /* R3.2:分段缓存根(book.c 派生 /seg/<hash>) */
|
||||||
|
}
|
||||||
|
|
||||||
int reader_open(const char *path)
|
int reader_open(const char *path)
|
||||||
{
|
{
|
||||||
if (!path || !path[0]) return -1;
|
if (!path || !path[0]) return -1;
|
||||||
@@ -661,6 +692,9 @@ int reader_open(const char *path)
|
|||||||
/* 阅读页底/字色:rd.bg(默认纯黑,B)→ 覆盖 th->surface */
|
/* 阅读页底/字色:rd.bg(默认纯黑,B)→ 覆盖 th->surface */
|
||||||
reader_resolve_bg(&g_page_bg, &g_page_fg);
|
reader_resolve_bg(&g_page_bg, &g_page_fg);
|
||||||
|
|
||||||
|
/* 页索引缓存(建好目录并注入;命中则开书免整书重排,秒开) */
|
||||||
|
reader_setup_index_dir();
|
||||||
|
|
||||||
/* 开书(FreeType 路径带齐几何;无字体则 g_has_font=0 → atlas 兜底) */
|
/* 开书(FreeType 路径带齐几何;无字体则 g_has_font=0 → atlas 兜底) */
|
||||||
int rc = rd_book_open(&g_book, path, fp, g_fs, g_ls, g_pw, g_ph, g_mx, g_my, g_top);
|
int rc = rd_book_open(&g_book, path, fp, g_fs, g_ls, g_pw, g_ph, g_mx, g_my, g_top);
|
||||||
if (rc != 0) {
|
if (rc != 0) {
|
||||||
@@ -807,6 +841,7 @@ void reader_on_tap(int x, int y)
|
|||||||
if (rd_book_set_enc(g_book, g_enc_cycle[ni]) == 0) {
|
if (rd_book_set_enc(g_book, g_enc_cycle[ni]) == 0) {
|
||||||
g_enc_idx = ni;
|
g_enc_idx = ni;
|
||||||
chapter_name();
|
chapter_name();
|
||||||
|
g_prog_dirty = 1;
|
||||||
reader_refill_page();
|
reader_refill_page();
|
||||||
reader_redraw();
|
reader_redraw();
|
||||||
reader_reflow_persist();
|
reader_reflow_persist();
|
||||||
@@ -900,5 +935,10 @@ void reader_on_tick(uint64_t now)
|
|||||||
if (g_book && !g_menu_open && !g_dir_open && !g_ui_open)
|
if (g_book && !g_menu_open && !g_dir_open && !g_ui_open)
|
||||||
reader_turn_animated(dir);
|
reader_turn_animated(dir);
|
||||||
}
|
}
|
||||||
|
/* R3.2:进度实时落盘(翻页/刮擦/字号/编码变更后,节流 ~1s 定时写)——意外崩溃重开不丢进度 */
|
||||||
|
if (g_prog_dirty && now - g_prog_last_save_ms >= 1000) {
|
||||||
|
g_prog_last_save_ms = now;
|
||||||
|
reader_reflow_persist();
|
||||||
|
}
|
||||||
if (g_notice_ms > 0) { g_notice_ms--; if (g_notice_ms == 0) reader_redraw(); }
|
if (g_notice_ms > 0) { g_notice_ms--; if (g_notice_ms == 0) reader_redraw(); }
|
||||||
}
|
}
|
||||||
@@ -91,6 +91,10 @@ echo "==> host core test (R1)"
|
|||||||
$HOST_CC -O2 -Wall -Wextra -Icore -I. test/test_reader_core.c core/rdenc.c core/rdlayout.c core/rdtxt.c -o test/test_reader_core_host -lm
|
$HOST_CC -O2 -Wall -Wextra -Icore -I. test/test_reader_core.c core/rdenc.c core/rdlayout.c core/rdtxt.c -o test/test_reader_core_host -lm
|
||||||
./test/test_reader_core_host
|
./test/test_reader_core_host
|
||||||
|
|
||||||
|
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 loaders test"
|
echo "==> host loaders test"
|
||||||
HOBJ="$OBJ/host"; mkdir -p "$HOBJ"
|
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
|
for f in vendor/miniz/miniz_tinfl.c vendor/miniz/miniz_tdef.c vendor/miniz/miniz_zip.c vendor/miniz/miniz.c; do
|
||||||
@@ -115,21 +119,21 @@ timeout 120 qemu-mipsel ./test/rdft_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-
|
|||||||
# ---------- 6) qemu-mipsel 书桥冒烟(TXT + 四格式) ----------
|
# ---------- 6) qemu-mipsel 书桥冒烟(TXT + 四格式) ----------
|
||||||
echo "==> mipsel book smoke (TXT)"
|
echo "==> mipsel book smoke (TXT)"
|
||||||
$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC test/book_smoke.c \
|
$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/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/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 \
|
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
|
"$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}"
|
timeout 120 qemu-mipsel ./test/book_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}"
|
||||||
|
|
||||||
echo "==> mipsel formats smoke"
|
echo "==> mipsel formats smoke"
|
||||||
$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC test/formats_smoke.c \
|
$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/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/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 \
|
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
|
"$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
|
timeout 150 qemu-mipsel ./test/formats_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}" /tmp/rd_samples
|
||||||
|
|
||||||
# ---------- 7) reader 二进制(mipsel 静态) ----------
|
# ---------- 7) reader 二进制(mipsel 静态) ----------
|
||||||
echo "==> reader binary"
|
echo "==> reader binary"
|
||||||
CORE_L="core/rdenc.c core/rdlayout.c core/rdtxt.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/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"
|
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"
|
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 \
|
$CC -Os -static -Wall -Wextra -I../sdk -I../meow_launcherd -I. -Icore -Iapp -Iengine $FT_INCS $COREINC \
|
||||||
|
|||||||
@@ -35,6 +35,39 @@ int rd_utf8_validate(const uint8_t *buf, size_t len)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 样本宽容 UTF-8 校验:与 rd_utf8_validate 同构,但对「缓冲末尾截断的多字节序列」按合法处理。
|
||||||
|
* 用途:rd_enc_detect 的样本是文件前 64KB 前窗,大 UTF-8 书几乎必然在该处把一个多字节汉字
|
||||||
|
* 切成两半;严格校验会因末尾半个字返回 0 → 误落 GB18030 → 全书乱码。此处仅放宽「结尾不完整
|
||||||
|
* 序列」,任何中间非法字节(overlong/代理/孤立续字节/非法首字节)仍返回 0,故 GBK/GB18030
|
||||||
|
* 样本不会因此被误判为 UTF-8(GBK 字节对在中途即因非法首/续字节被拒)。 */
|
||||||
|
static int rd_utf8_sample_ok(const uint8_t *buf, size_t len)
|
||||||
|
{
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < len) {
|
||||||
|
uint8_t b = buf[i];
|
||||||
|
if (b < 0x80) { i++; continue; }
|
||||||
|
unsigned n, lo, hi;
|
||||||
|
if ((b & 0xE0) == 0xC0) { n = 2; lo = 0x80; hi = 0xBF; }
|
||||||
|
else if ((b & 0xF0) == 0xE0) { n = 3; lo = 0x80; hi = 0xBF; }
|
||||||
|
else if ((b & 0xF8) == 0xF0) { n = 4; lo = 0x80; hi = 0xBF; }
|
||||||
|
else return 0;
|
||||||
|
if (n == 2 && (b & 0xFE) == 0xC0) return 0; /* C0/C1 overlong */
|
||||||
|
if (n == 3 && b == 0xE0) lo = 0xA0; /* 防 overlong < U+0800 */
|
||||||
|
if (n == 3 && b == 0xED) hi = 0x9F; /* 防代理 U+D800..DFFF */
|
||||||
|
if (n == 4 && b == 0xF0) lo = 0x90; /* 防 overlong < U+10000 */
|
||||||
|
if (n == 4 && b == 0xF4) hi = 0x8F; /* 防 > U+10FFFF */
|
||||||
|
if (i + n > len) return 1; /* 末尾截断:宽容(前窗裁掉一半) */
|
||||||
|
for (unsigned k = 1; k < n; k++) {
|
||||||
|
uint8_t c = buf[i + k];
|
||||||
|
uint8_t cl = (k == 1) ? (uint8_t)lo : 0x80;
|
||||||
|
uint8_t ch = (k == 1) ? (uint8_t)hi : 0xBF;
|
||||||
|
if (c < cl || c > ch) return 0;
|
||||||
|
}
|
||||||
|
i += n;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* ================= GB18030 双字节 → UTF-8 ================= */
|
/* ================= GB18030 双字节 → UTF-8 ================= */
|
||||||
static inline int gb_trail_ok(unsigned b)
|
static inline int gb_trail_ok(unsigned b)
|
||||||
{
|
{
|
||||||
@@ -186,8 +219,8 @@ rd_enc_t rd_enc_detect(const uint8_t *buf, size_t len, rd_enc_stats *st)
|
|||||||
if (len >= 2 && buf[0] == 0xFF && buf[1] == 0xFE) return RD_ENC_UTF16LE;
|
if (len >= 2 && buf[0] == 0xFF && buf[1] == 0xFE) return RD_ENC_UTF16LE;
|
||||||
if (len >= 2 && buf[0] == 0xFE && buf[1] == 0xFF) return RD_ENC_UTF16BE;
|
if (len >= 2 && buf[0] == 0xFE && buf[1] == 0xFF) return RD_ENC_UTF16BE;
|
||||||
|
|
||||||
/* 2) 严格 UTF-8 */
|
/* 2) 严格 UTF-8(样本宽容末尾截断,见 rd_utf8_sample_ok 注释) */
|
||||||
if (rd_utf8_validate(buf, len)) {
|
if (rd_utf8_sample_ok(buf, len)) {
|
||||||
/* 纯 ASCII 判 ASCII(信息性;UTF-8 子集) */
|
/* 纯 ASCII 判 ASCII(信息性;UTF-8 子集) */
|
||||||
int ascii = 1;
|
int ascii = 1;
|
||||||
for (size_t i = 0; i < len; i++) if (buf[i] >= 0x80) { ascii = 0; break; }
|
for (size_t i = 0; i < len; i++) if (buf[i] >= 0x80) { ascii = 0; break; }
|
||||||
|
|||||||
@@ -74,15 +74,32 @@ int rdtxt_reader_next(rdtxt_reader *r, uint8_t *utf8, size_t cap,
|
|||||||
long start = ftell(r->f);
|
long start = ftell(r->f);
|
||||||
if (start < 0) { r->eof = 1; return 1; }
|
if (start < 0) { r->eof = 1; return 1; }
|
||||||
|
|
||||||
|
/* fread 块扫替代逐字节 fgetc:大书扫描 O(filesize) 从全量 fgetc(逐字符 libc 调用+换行判定)
|
||||||
|
* 改为块读+memchr,显著降低开书/建段表代价(原 fgetc 是 UI 线程被占「返回无效」成因之一)。
|
||||||
|
* 语义与旧版一致:段 = 到达 UTF8_MAX/2 后首个 `\n` 为止(含该 `\n`)的字节;无 `\n` 则读满
|
||||||
|
* rawsz-1;EOF 以 fread 返回 0 判定。 */
|
||||||
size_t raw_n = 0;
|
size_t raw_n = 0;
|
||||||
int got = 0;
|
int got = 0;
|
||||||
while (raw_n < r->rawsz - 1) {
|
while (raw_n < r->rawsz - 1) {
|
||||||
int c = fgetc(r->f);
|
size_t n0 = fread(r->raw + raw_n, 1, (r->rawsz - 1) - raw_n, r->f);
|
||||||
if (c == EOF) { r->eof = 1; break; }
|
if (n0 == 0) { if (feof(r->f)) r->eof = 1; break; }
|
||||||
r->raw[raw_n++] = (uint8_t)c;
|
|
||||||
got = 1;
|
got = 1;
|
||||||
/* 段落边界达成目标字节约停止(`\n` 恒为独立换行) */
|
size_t chunk_end = raw_n + n0;
|
||||||
if (c == '\n' && raw_n >= RDTXT_SEG_UTF8_MAX / 2) break;
|
size_t from = (raw_n < RDTXT_SEG_UTF8_MAX / 2) ? (RDTXT_SEG_UTF8_MAX / 2) : raw_n;
|
||||||
|
if (chunk_end >= RDTXT_SEG_UTF8_MAX / 2) {
|
||||||
|
int stop = 0;
|
||||||
|
for (size_t k = from; k < chunk_end; k++) {
|
||||||
|
if (r->raw[k] == '\n') {
|
||||||
|
size_t keep = k + 1; /* 本段含该 `\n` */
|
||||||
|
if (fseek(r->f, start + (long)keep, SEEK_SET) != 0) r->eof = 1;
|
||||||
|
raw_n = keep;
|
||||||
|
stop = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stop) break;
|
||||||
|
}
|
||||||
|
raw_n = chunk_end;
|
||||||
}
|
}
|
||||||
if (!got) { r->eof = 1; return 1; }
|
if (!got) { r->eof = 1; return 1; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
/* core/seg.c — TXT 分段流式缓存实现(见 core/seg.h)。
|
||||||
|
* 纯 libc(fopen/fread/fwrite/stat/remove/mkdir)+ rdenc 的码位边界对齐,无 SDK 依赖。
|
||||||
|
*/
|
||||||
|
#include "seg.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#define SEG_MAGIC0 'R'
|
||||||
|
#define SEG_MAGIC1 'D'
|
||||||
|
#define SEG_MAGIC2 'S'
|
||||||
|
#define SEG_MAGIC3 'G'
|
||||||
|
#define SEG_VER 1u
|
||||||
|
|
||||||
|
static char g_base[400];
|
||||||
|
|
||||||
|
void rdseg_set_base(const char *base)
|
||||||
|
{
|
||||||
|
if (base && base[0]) {
|
||||||
|
strncpy(g_base, base, sizeof g_base - 1);
|
||||||
|
g_base[sizeof g_base - 1] = 0;
|
||||||
|
} else {
|
||||||
|
g_base[0] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t seg_fnv(const char *s)
|
||||||
|
{
|
||||||
|
uint32_t h = 2166136261u;
|
||||||
|
for (const unsigned char *p = (const unsigned char *)s; *p; p++) { h ^= *p; h *= 16777619u; }
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rdseg_dir_for(const char *book_path, char *out, int outsz)
|
||||||
|
{
|
||||||
|
if (!book_path || !out || !g_base[0]) return -1;
|
||||||
|
if (snprintf(out, (size_t)outsz, "%s/seg/%08x", g_base, seg_fnv(book_path)) >= outsz) return -2;
|
||||||
|
/* mkdir -p 幂等(progress/index 同款) */
|
||||||
|
char t[520];
|
||||||
|
strncpy(t, out, sizeof t - 1); t[sizeof t - 1] = 0;
|
||||||
|
for (char *c = t + 1; *c; c++) {
|
||||||
|
if (*c == '/') { *c = 0; mkdir(t, 0755); *c = '/'; }
|
||||||
|
}
|
||||||
|
mkdir(t, 0755);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 码位边界对齐 ---- */
|
||||||
|
static int b5_trail(unsigned b) { return (b >= 0x40 && b <= 0x7E) || (b >= 0xA1 && b <= 0xFE); }
|
||||||
|
static int gb_trail(unsigned b) { return (b >= 0x40 && b <= 0x7E) || (b >= 0x80 && b <= 0xFE); }
|
||||||
|
|
||||||
|
/* 返回 raw[0..len) 中 ≤ target 的最大「码位边界」cut(该处之后是完整字符;cut 越小越保守)。
|
||||||
|
* target 越界钳到 len。 */
|
||||||
|
static size_t seg_align(rd_enc_t enc, const uint8_t *raw, size_t len, size_t target)
|
||||||
|
{
|
||||||
|
if (target > len) target = len;
|
||||||
|
if (enc == RD_ENC_UTF16LE || enc == RD_ENC_UTF16BE) {
|
||||||
|
size_t cut = target & ~(size_t)1; /* 偶数对齐 */
|
||||||
|
if (cut >= 2) {
|
||||||
|
uint16_t u = (enc == RD_ENC_UTF16BE) ? (uint16_t)((raw[cut - 2] << 8) | raw[cut - 1])
|
||||||
|
: (uint16_t)((raw[cut - 1] << 8) | raw[cut - 2]);
|
||||||
|
if (u >= 0xD800 && u <= 0xDBFF) cut -= 2; /* 高代理在边界 → 携带到下一段 */
|
||||||
|
}
|
||||||
|
return cut;
|
||||||
|
}
|
||||||
|
size_t p = 0, last = 0;
|
||||||
|
while (p < target) {
|
||||||
|
unsigned n = 1;
|
||||||
|
uint8_t b = raw[p];
|
||||||
|
if (enc == RD_ENC_UTF8 || enc == RD_ENC_ASCII) {
|
||||||
|
if (b < 0x80) n = 1;
|
||||||
|
else if ((b & 0xE0) == 0xC0) n = 2;
|
||||||
|
else if ((b & 0xF0) == 0xE0) n = 3;
|
||||||
|
else if ((b & 0xF8) == 0xF0) n = 4;
|
||||||
|
else n = 1;
|
||||||
|
} else { /* GB18030 / BIG5 双字节 */
|
||||||
|
if (b < 0x80) n = 1;
|
||||||
|
else if (b >= 0x81 && b <= 0xFE && p + 1 < len) {
|
||||||
|
uint8_t t = raw[p + 1];
|
||||||
|
n = ((enc == RD_ENC_BIG5) ? b5_trail(t) : gb_trail(t)) ? 2u : 1u;
|
||||||
|
} else n = 1;
|
||||||
|
}
|
||||||
|
if (p + n > target) break; /* 越过 target → 停在上一安全位 */
|
||||||
|
p += n; last = p;
|
||||||
|
}
|
||||||
|
return last ? last : target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- manifest IO ---- */
|
||||||
|
static int manifest_path(const char *seg_dir, char *out, int outsz)
|
||||||
|
{
|
||||||
|
if (!seg_dir || !seg_dir[0]) return -1;
|
||||||
|
return (snprintf(out, (size_t)outsz, "%s/manifest.idx", seg_dir) >= outsz) ? -2 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int manifest_valid(const char *seg_dir, uint64_t fsize, int64_t fmtime, rd_enc_t enc, int *nsegs)
|
||||||
|
{
|
||||||
|
char man[560];
|
||||||
|
if (manifest_path(seg_dir, man, sizeof man) != 0) return 0;
|
||||||
|
FILE *mf = fopen(man, "rb");
|
||||||
|
if (!mf) return 0;
|
||||||
|
uint8_t magic[4];
|
||||||
|
uint16_t ver;
|
||||||
|
uint64_t mfs, mmt;
|
||||||
|
uint32_t menc, mn;
|
||||||
|
int ok = 0;
|
||||||
|
if (fread(magic, 1, 4, mf) == 4 && magic[0] == SEG_MAGIC0 && magic[1] == SEG_MAGIC1 &&
|
||||||
|
magic[2] == SEG_MAGIC2 && magic[3] == SEG_MAGIC3 &&
|
||||||
|
fread(&ver, 2, 1, mf) == 1 && ver == SEG_VER &&
|
||||||
|
fread(&mfs, 8, 1, mf) == 1 && fread(&mmt, 8, 1, mf) == 1 &&
|
||||||
|
fread(&menc, 4, 1, mf) == 1 && fread(&mn, 4, 1, mf) == 1 &&
|
||||||
|
mfs == fsize && mmt == (uint64_t)fmtime && menc == (uint32_t)enc && mn >= 1 && mn < 0x7fffffffu) {
|
||||||
|
if (nsegs) *nsegs = (int)mn;
|
||||||
|
ok = 1;
|
||||||
|
}
|
||||||
|
fclose(mf);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rdseg_n(const char *seg_dir)
|
||||||
|
{
|
||||||
|
char man[560];
|
||||||
|
if (manifest_path(seg_dir, man, sizeof man) != 0) return 0;
|
||||||
|
FILE *mf = fopen(man, "rb");
|
||||||
|
if (!mf) return 0;
|
||||||
|
uint8_t magic[4]; uint16_t ver; uint32_t mn;
|
||||||
|
int n = 0;
|
||||||
|
if (fread(magic, 1, 4, mf) == 4 && magic[0] == SEG_MAGIC0 && magic[1] == SEG_MAGIC1 &&
|
||||||
|
magic[2] == SEG_MAGIC2 && magic[3] == SEG_MAGIC3 &&
|
||||||
|
fread(&ver, 2, 1, mf) == 1 && ver == SEG_VER &&
|
||||||
|
fseek(mf, 8 + 8 + 4, SEEK_CUR) == 0 &&
|
||||||
|
fread(&mn, 4, 1, mf) == 1 && mn >= 1 && mn < 0x7fffffffu) n = (int)mn;
|
||||||
|
fclose(mf);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rdseg_rawlen(const char *seg_dir, int i, size_t *len)
|
||||||
|
{
|
||||||
|
if (!seg_dir || !len || i < 0) return -1;
|
||||||
|
char man[560];
|
||||||
|
if (manifest_path(seg_dir, man, sizeof man) != 0) return -2;
|
||||||
|
FILE *mf = fopen(man, "rb");
|
||||||
|
if (!mf) return 1;
|
||||||
|
uint8_t magic[4]; uint16_t ver; uint32_t mn;
|
||||||
|
int r = 1;
|
||||||
|
if (fread(magic, 1, 4, mf) == 4 && magic[0] == SEG_MAGIC0 &&
|
||||||
|
fread(&ver, 2, 1, mf) == 1 && ver == SEG_VER &&
|
||||||
|
fseek(mf, 8 + 8 + 4, SEEK_CUR) == 0 &&
|
||||||
|
fread(&mn, 4, 1, mf) == 1 && (uint32_t)i < mn) {
|
||||||
|
int ok = 1;
|
||||||
|
for (int k = 0; k < i; k++) {
|
||||||
|
uint64_t a, b;
|
||||||
|
if (fread(&a, 8, 1, mf) != 1 || fread(&b, 8, 1, mf) != 1) { ok = 0; break; }
|
||||||
|
}
|
||||||
|
if (ok) {
|
||||||
|
uint64_t a, b;
|
||||||
|
if (fread(&a, 8, 1, mf) == 1 && fread(&b, 8, 1, mf) == 1) { *len = (size_t)b; r = 0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(mf);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rdseg_build(const char *book_path, rd_enc_t enc, const char *seg_dir)
|
||||||
|
{
|
||||||
|
if (!book_path || !seg_dir || !g_base[0]) return 0;
|
||||||
|
struct stat st;
|
||||||
|
if (stat(book_path, &st) != 0) return -1;
|
||||||
|
uint64_t fsize = (uint64_t)st.st_size;
|
||||||
|
int64_t fmtime = (int64_t)st.st_mtime;
|
||||||
|
if (fsize == 0) return 0; /* 空书不切 */
|
||||||
|
|
||||||
|
int n_cached = 0;
|
||||||
|
if (manifest_valid(seg_dir, fsize, fmtime, enc, &n_cached)) return n_cached;
|
||||||
|
|
||||||
|
FILE *src = fopen(book_path, "rb");
|
||||||
|
if (!src) return -1;
|
||||||
|
|
||||||
|
size_t pending_cap = RDSEG_SIZE + 65536 + 4;
|
||||||
|
uint8_t *pending = (uint8_t *)malloc(pending_cap);
|
||||||
|
if (!pending) { fclose(src); return -6; }
|
||||||
|
uint8_t row[65536];
|
||||||
|
size_t pend = 0;
|
||||||
|
|
||||||
|
typedef struct { uint64_t raw_off, raw_len; } ent_t;
|
||||||
|
ent_t *ents = NULL; size_t ne = 0, cape = 0;
|
||||||
|
uint64_t raw_off = 0;
|
||||||
|
int seg_index = 0;
|
||||||
|
int err = 0;
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
size_t got = fread(row, 1, sizeof row, src);
|
||||||
|
if (got + pend > pending_cap) { err = -3; break; }
|
||||||
|
memcpy(pending + pend, row, got);
|
||||||
|
pend += got;
|
||||||
|
while (pend >= RDSEG_SIZE) {
|
||||||
|
size_t cut = seg_align(enc, pending, pend, RDSEG_SIZE);
|
||||||
|
if (cut == 0) cut = RDSEG_SIZE; /* 无法对齐(畸形输入)→ 兜底整段 */
|
||||||
|
char name[32]; snprintf(name, sizeof name, "seg_%03d.dat", seg_index);
|
||||||
|
char full[600];
|
||||||
|
if (snprintf(full, sizeof full, "%s/%s", seg_dir, name) >= (int)sizeof full) { err = -3; break; }
|
||||||
|
FILE *o = fopen(full, "wb");
|
||||||
|
if (!o) { err = -3; break; }
|
||||||
|
fwrite(pending, 1, cut, o);
|
||||||
|
fclose(o);
|
||||||
|
if (ne >= cape) { cape = cape ? cape * 2 : 64; ent_t *no = (ent_t *)realloc(ents, cape * sizeof *no); if (!no) { err = -6; break; } ents = no; }
|
||||||
|
ents[ne].raw_off = raw_off; ents[ne].raw_len = cut; ne++;
|
||||||
|
raw_off += cut; seg_index++;
|
||||||
|
memmove(pending, pending + cut, pend - cut);
|
||||||
|
pend -= cut;
|
||||||
|
if (err) break;
|
||||||
|
if (pend == 0) break;
|
||||||
|
}
|
||||||
|
if (err) break;
|
||||||
|
if (got < sizeof row) break; /* EOF */
|
||||||
|
}
|
||||||
|
fclose(src);
|
||||||
|
|
||||||
|
if (!err && pend > 0) { /* 末段(不足 512KB) */
|
||||||
|
char name[32]; snprintf(name, sizeof name, "seg_%03d.dat", seg_index);
|
||||||
|
char full[600];
|
||||||
|
if (snprintf(full, sizeof full, "%s/%s", seg_dir, name) < (int)sizeof full) {
|
||||||
|
FILE *o = fopen(full, "wb");
|
||||||
|
if (!o) err = -3;
|
||||||
|
else { fwrite(pending, 1, pend, o); fclose(o);
|
||||||
|
if (ne >= cape) { cape = cape ? cape * 2 : 64; ent_t *no = (ent_t *)realloc(ents, cape * sizeof *no); if (!no) { err = -6; } else ents = no; }
|
||||||
|
if (!err) { ents[ne].raw_off = raw_off; ents[ne].raw_len = pend; ne++; raw_off += pend; seg_index++; } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(pending);
|
||||||
|
|
||||||
|
if (!err) {
|
||||||
|
char man[560];
|
||||||
|
if (manifest_path(seg_dir, man, sizeof man) == 0) {
|
||||||
|
FILE *mf = fopen(man, "wb");
|
||||||
|
if (!mf) err = -4;
|
||||||
|
else {
|
||||||
|
uint8_t magic[4] = { SEG_MAGIC0, SEG_MAGIC1, SEG_MAGIC2, SEG_MAGIC3 };
|
||||||
|
uint16_t ver = SEG_VER;
|
||||||
|
uint32_t uenc = (uint32_t)enc, un = (uint32_t)ne;
|
||||||
|
fwrite(magic, 1, 4, mf); fwrite(&ver, 2, 1, mf);
|
||||||
|
fwrite(&fsize, 8, 1, mf); fwrite(&fmtime, 8, 1, mf);
|
||||||
|
fwrite(&uenc, 4, 1, mf); fwrite(&un, 4, 1, mf);
|
||||||
|
for (size_t k = 0; k < ne; k++) { fwrite(&ents[k].raw_off, 8, 1, mf); fwrite(&ents[k].raw_len, 8, 1, mf); }
|
||||||
|
fclose(mf);
|
||||||
|
}
|
||||||
|
} else err = -2;
|
||||||
|
}
|
||||||
|
free(ents);
|
||||||
|
return err ? err : (int)ne;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rdseg_load(const char *seg_dir, int i, uint8_t *buf, size_t cap, size_t *len)
|
||||||
|
{
|
||||||
|
if (!seg_dir || !buf || !len || i < 0) return -1;
|
||||||
|
size_t rl = 0;
|
||||||
|
if (rdseg_rawlen(seg_dir, i, &rl) != 0) return 1;
|
||||||
|
if (rl > cap) return -3;
|
||||||
|
char name[32]; snprintf(name, sizeof name, "seg_%03d.dat", i);
|
||||||
|
char full[600];
|
||||||
|
if (snprintf(full, sizeof full, "%s/%s", seg_dir, name) >= (int)sizeof full) return -2;
|
||||||
|
FILE *f = fopen(full, "rb");
|
||||||
|
if (!f) return -4;
|
||||||
|
size_t g = fread(buf, 1, rl, f);
|
||||||
|
fclose(f);
|
||||||
|
if (g != rl) return -5;
|
||||||
|
*len = g;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void rdseg_clear(const char *seg_dir)
|
||||||
|
{
|
||||||
|
if (!seg_dir || !seg_dir[0]) return;
|
||||||
|
char man[560];
|
||||||
|
if (manifest_path(seg_dir, man, sizeof man) == 0) remove(man);
|
||||||
|
for (int i = 0; i < 100000; i++) {
|
||||||
|
char n[32]; snprintf(n, sizeof n, "%s/seg_%03d.dat", seg_dir, i);
|
||||||
|
if (remove(n) != 0) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/* core/seg.h — TXT 分段流式缓存:把 txt(任意 rd_enc_t 编码)原始字节按 ~512KB 对齐到「码位边界」
|
||||||
|
* 切成多个段文件(`<TF>/MeowData/os.meow.reader/seg/<path哈希>/seg_NNN.dat`)+ 一个 manifest.idx
|
||||||
|
* (记录每段起始字节偏移 + 段长,加载时按偏移拼接/定位)。用于:
|
||||||
|
* 1. 建索引时顺带切段落盘(复用 index 同父目录,无卡/未配置 base 时直接禁用、不持久);
|
||||||
|
* 2. 渲染只加载「当前页所在段」这一个段文件(有界 ~512KB,不全量读整书)——治大书开书
|
||||||
|
* CPU 100%/返回无效(`build_index` 全量建表 + `refill` 全量光栅化占用 UI 线程)。
|
||||||
|
*
|
||||||
|
* 纯 libc + rdenc,无 SDK 依赖;宿主可测。段内字节对齐到该编码的码位边界(UTF-8/GB18030/Big5
|
||||||
|
* 按首字节序列,UTF-16 按偶数对齐并避免在代理对中间断开),故加载时 `rd_to_utf8` 转换不截断
|
||||||
|
* 多字节字符(尾字完整)。
|
||||||
|
*/
|
||||||
|
#ifndef RDSEG_H
|
||||||
|
#define RDSEG_H
|
||||||
|
|
||||||
|
#include "rdenc.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* 每段原始字节目标(对齐到码位边界后小幅 ±)。 */
|
||||||
|
#define RDSEG_SIZE (512 * 1024)
|
||||||
|
|
||||||
|
/* 配置段缓存根 base(= <TF>/MeowData/os.meow.reader,与 index 同父),由 reader.c 注入;
|
||||||
|
* base 为空/未设 → 全程禁用(直排不持久,等同 index 关闭)。 */
|
||||||
|
void rdseg_set_base(const char *base);
|
||||||
|
|
||||||
|
/* 由 book_path 推导段目录 `<base>/seg/<path哈希>` 并 mkdir -p。
|
||||||
|
* base 禁用 → 返回 -1(out 不动);成功 0(out 填),失败负值。 */
|
||||||
|
int rdseg_dir_for(const char *book_path, char *out, int outsz);
|
||||||
|
|
||||||
|
/* 建段文件 + manifest(幂等:manifest 键 (file_size, mtime, enc) 与当前一致且完整 → 不重写,返回已有 nsegs)。
|
||||||
|
* 成功返回段数 nsegs(>=1);base 禁用/空书 0;错误负值。 */
|
||||||
|
int rdseg_build(const char *book_path, rd_enc_t enc, const char *seg_dir);
|
||||||
|
|
||||||
|
/* manifest 段数;段目录不存在/空 → 0。 */
|
||||||
|
int rdseg_n(const char *seg_dir);
|
||||||
|
|
||||||
|
/* 段 i 原始字节长(manifest)。0=成功;越界 1;错误负值。 */
|
||||||
|
int rdseg_rawlen(const char *seg_dir, int i, size_t *len);
|
||||||
|
|
||||||
|
/* 读段 i 原始字节到 buf(cap 须 ≥ 段长)。0=成功(*len);越界 1;错误负值。 */
|
||||||
|
int rdseg_load(const char *seg_dir, int i, uint8_t *buf, size_t cap, size_t *len);
|
||||||
|
|
||||||
|
/* 删除段目录全部文件(编码切换等重建前用)。 */
|
||||||
|
void rdseg_clear(const char *seg_dir);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif /* RDSEG_H */
|
||||||
@@ -31,6 +31,9 @@ int main(int argc, char **argv)
|
|||||||
write_txt(txt);
|
write_txt(txt);
|
||||||
|
|
||||||
rd_book *b = NULL;
|
rd_book *b = NULL;
|
||||||
|
/* R3.2:启用分段流式缓存(/tmp),走「切 512KB 段 + 按需布局」路径,而非全量直排 */
|
||||||
|
rd_book_set_seg_base("/tmp/meow_seg_smoke");
|
||||||
|
rd_book_set_index_dir("/tmp/meow_seg_smoke_index");
|
||||||
int rc = rd_book_open(&b, txt, font, 20, 130, 480, 720, 16, 20, 62);
|
int rc = rd_book_open(&b, txt, font, 20, 130, 480, 720, 16, 20, 62);
|
||||||
CHECK(rc == 0, "rd_book_open");
|
CHECK(rc == 0, "rd_book_open");
|
||||||
if (rc != 0) { printf("open rc=%d\n", rc); return 1; }
|
if (rc != 0) { printf("open rc=%d\n", rc); return 1; }
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/* test/seg_smoke.c — os.meow.reader 分段流式缓存宿主单测(纯 C,无 SDK/Freetype)。
|
||||||
|
*
|
||||||
|
* 覆盖:
|
||||||
|
* S1 UTF-8 大文件按 ~512KB 切段:段数>1、每段为合法 UTF-8(码位对齐,不切在字符中间)、
|
||||||
|
* 拼接后 == 原文件字节。
|
||||||
|
* S2 GBK(GB18030) 大文件切段:每段 raw 在完整双字节边界、逐段 rd_to_utf8 拼接 == 整文件
|
||||||
|
* 一次转换(边界不把双字节对拆开)。
|
||||||
|
* S3 幂等:同 (size,mtime,enc) 再次 build 段数不变、不重写。
|
||||||
|
* S4 每段原始字节长与 rdseg_load 读回一致。
|
||||||
|
*/
|
||||||
|
#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)
|
||||||
|
|
||||||
|
/* 生成一个 utf8 大文件:CJK 3 字节 + ASCII + 换行,填充到 > 512KB */
|
||||||
|
static void write_utf8_big(const char *path)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "wb");
|
||||||
|
const uint8_t cjk[] = { 0xE4, 0xBD, 0xA0, 0xE5, 0xA5, 0xBD }; /* 你好 */
|
||||||
|
size_t total = 0;
|
||||||
|
while (total < RDSEG_SIZE + 100000) {
|
||||||
|
for (int i = 0; i < 16; i++) { fwrite(cjk, 1, sizeof cjk, f); total += sizeof cjk; }
|
||||||
|
const char *line = "这是一段用于验证分段对齐的中文正文,English words 123.\n";
|
||||||
|
size_t ll = strlen(line); fwrite(line, 1, ll, f); total += ll;
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 生成一个 GBK 大文件:双字节对(你好世界)重复,填到 > 512KB */
|
||||||
|
static void write_gbk_big(const char *path)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "wb");
|
||||||
|
const uint8_t gbk[] = { 0xC4, 0xE3, 0xBA, 0xC3, 0xCA, 0xC0, 0xBD, 0xE7 }; /* 你好世界 */
|
||||||
|
size_t total = 0;
|
||||||
|
while (total < RDSEG_SIZE + 100000) {
|
||||||
|
for (int i = 0; i < 16; i++) { fwrite(gbk, 1, sizeof gbk, f); total += sizeof gbk; }
|
||||||
|
fputc('\n', f); total += 1;
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_align_utf8(const char *seg_dir, const char *path)
|
||||||
|
{
|
||||||
|
/* 每段合法 UTF-8 */
|
||||||
|
int n = rdseg_n(seg_dir);
|
||||||
|
CHECK(n >= 2, "utf8 segs >= 2");
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
size_t rl = 0;
|
||||||
|
CHECK(rdseg_rawlen(seg_dir, i, &rl) == 0 && rl > 0, "utf8 rawlen>0");
|
||||||
|
uint8_t *b = (uint8_t *)malloc(rl ? rl : 1);
|
||||||
|
size_t got = 0;
|
||||||
|
CHECK(rdseg_load(seg_dir, i, b, rl, &got) == 0 && got == rl, "utf8 load==rawlen");
|
||||||
|
CHECK(rd_utf8_validate(b, got) == 1, "utf8 seg valid (no mid-char split)");
|
||||||
|
free(b);
|
||||||
|
}
|
||||||
|
/* 拼接后 == 原文件字节 */
|
||||||
|
size_t fz = 0;
|
||||||
|
FILE *src = fopen(path, "rb"); fseek(src, 0, SEEK_END); fz = (size_t)ftell(src); fseek(src, 0, SEEK_SET);
|
||||||
|
uint8_t *full = (uint8_t *)malloc(fz ? fz : 1);
|
||||||
|
(void)!fread(full, 1, fz, src); fclose(src);
|
||||||
|
size_t o = 0, ok = 1;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
size_t rl = 0; rdseg_rawlen(seg_dir, i, &rl);
|
||||||
|
uint8_t *b = (uint8_t *)malloc(rl ? rl : 1);
|
||||||
|
size_t got = 0; rdseg_load(seg_dir, i, b, rl, &got);
|
||||||
|
if (o + got > fz || memcmp(full + o, b, got) != 0) ok = 0;
|
||||||
|
o += got; free(b);
|
||||||
|
}
|
||||||
|
CHECK(ok && o == fz, "utf8 concat == original bytes");
|
||||||
|
free(full);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_gbk_roundtrip(const char *seg_dir, const char *path)
|
||||||
|
{
|
||||||
|
int n = rdseg_n(seg_dir);
|
||||||
|
CHECK(n >= 2, "gbk segs >= 2");
|
||||||
|
/* 整文件一次转换(金标准) */
|
||||||
|
FILE *src = fopen(path, "rb"); fseek(src, 0, SEEK_END); long fz = ftell(src); fseek(src, 0, SEEK_SET);
|
||||||
|
uint8_t *raw = (uint8_t *)malloc((size_t)fz ? (size_t)fz : 1);
|
||||||
|
(void)!fread(raw, 1, (size_t)fz, src); fclose(src);
|
||||||
|
uint8_t *full = (uint8_t *)malloc((size_t)fz * 2 + 64);
|
||||||
|
size_t fl = 0;
|
||||||
|
int rc = rd_gb18030_to_utf8(raw, (size_t)fz, full, (size_t)fz * 2 + 64, &fl);
|
||||||
|
CHECK(rc == RD_ENC_OK, "gbk full convert ok");
|
||||||
|
free(raw);
|
||||||
|
/* 逐段转换拼接 */
|
||||||
|
uint8_t *concat = (uint8_t *)malloc((size_t)fz * 2 + 64);
|
||||||
|
size_t o = 0, ok = 1;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
size_t rl = 0; rdseg_rawlen(seg_dir, i, &rl);
|
||||||
|
uint8_t *b = (uint8_t *)malloc(rl ? rl : 1);
|
||||||
|
size_t got = 0; rdseg_load(seg_dir, i, b, rl, &got);
|
||||||
|
uint8_t *u8 = (uint8_t *)malloc(got * 2 + 64);
|
||||||
|
size_t ul = 0;
|
||||||
|
if (rd_gb18030_to_utf8(b, got, u8, got * 2 + 64, &ul) != RD_ENC_OK) ok = 0;
|
||||||
|
if (o + ul > (size_t)fz * 2 + 64) ok = 0;
|
||||||
|
if (ok) { memcpy(concat + o, u8, ul); o += ul; }
|
||||||
|
free(u8); free(b);
|
||||||
|
}
|
||||||
|
CHECK(ok && o == fl && memcmp(concat, full, fl) == 0, "gbk per-seg convert concat == full convert");
|
||||||
|
free(concat); free(full);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
rdseg_set_base("/tmp/meow_seg_base");
|
||||||
|
char dir[600];
|
||||||
|
const char *u8 = "/tmp/seg_u8.txt", *gbk = "/tmp/seg_gbk.txt";
|
||||||
|
write_utf8_big(u8); write_gbk_big(gbk);
|
||||||
|
|
||||||
|
/* UTF-8 */
|
||||||
|
CHECK(rdseg_dir_for(u8, dir, sizeof dir) == 0, "utf8 dir_for");
|
||||||
|
int n = rdseg_build(u8, RD_ENC_UTF8, dir);
|
||||||
|
CHECK(n >= 2, "utf8 build nsegs>=2");
|
||||||
|
test_align_utf8(dir, u8);
|
||||||
|
/* 幂等:同键再建段数不变 */
|
||||||
|
CHECK(rdseg_build(u8, RD_ENC_UTF8, dir) == n, "utf8 build idempotent");
|
||||||
|
|
||||||
|
/* GBK */
|
||||||
|
CHECK(rdseg_dir_for(gbk, dir, sizeof dir) == 0, "gbk dir_for");
|
||||||
|
n = rdseg_build(gbk, RD_ENC_GB18030, dir);
|
||||||
|
CHECK(n >= 2, "gbk build nsegs>=2");
|
||||||
|
test_gbk_roundtrip(dir, gbk);
|
||||||
|
|
||||||
|
/* 大文件不切(清空再建,确保得到多个段也覆盖单段路径) */
|
||||||
|
{
|
||||||
|
char small[600]; rdseg_set_base("/tmp/meow_seg_base2");
|
||||||
|
const char *tiny = "/tmp/seg_tiny.txt";
|
||||||
|
FILE *f = fopen(tiny, "wb"); fwrite("hello 你好 world\n", 1, strlen("hello 你好 world\n"), f); fclose(f);
|
||||||
|
CHECK(rdseg_dir_for(tiny, small, sizeof small) == 0, "tiny dir");
|
||||||
|
int tn = rdseg_build(tiny, RD_ENC_UTF8, small);
|
||||||
|
CHECK(tn == 1, "tiny build nsegs==1");
|
||||||
|
size_t rl = 0; CHECK(rdseg_rawlen(small, 0, &rl) == 0 && rl == strlen("hello 你好 world\n"), "tiny rawlen");
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(u8); remove(gbk); remove("/tmp/seg_tiny.txt");
|
||||||
|
printf("seg_smoke: %d pass, %d fail\n", g_pass, g_fail);
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -322,10 +322,29 @@ static void t12(void)
|
|||||||
remove(path);
|
remove(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* t13 — 大 UTF-8 书编码检测:前 64KB 窗口恰在多字节字符中间截断时仍判 UTF-8。
|
||||||
|
* 复现 M6.42《这游戏》乱码:rdtxt_probe 读前 64KB → rd_enc_detect 严格 UTF-8 校验因末尾
|
||||||
|
* 半个汉字失败 → 落入 GB18030 兜底 → 全书按 GB18030 解码 → 乱码。此处必须判回 UTF-8。 */
|
||||||
|
static void t13(void)
|
||||||
|
{
|
||||||
|
uint8_t buf[65536];
|
||||||
|
const uint8_t cjk[] = { 0xE4, 0xBD, 0xA0 }; /* 「你」= 3 字节 CJK */
|
||||||
|
size_t o = 0;
|
||||||
|
while (o + 3 <= 65535) { memcpy(buf + o, cjk, 3); o += 3; } /* o == 65535 */
|
||||||
|
buf[o] = 0xE4; /* 末尾 1 字节 = 半个汉字(3 字节序列被 64KB 窗口切断) */
|
||||||
|
size_t len = o + 1; /* 65536:rdtxt_probe 读满 64KB */
|
||||||
|
CHECK(len == 65536, "t13 len");
|
||||||
|
CHECK(rd_utf8_validate(buf, len) == 0, "t13 strict rejects trailing truncation");
|
||||||
|
CHECK(rd_enc_detect(buf, len, NULL) == RD_ENC_UTF8, "t13 detect utf8 despite mid-char cut");
|
||||||
|
/* 对照:GBK 仍须判 GB18030(样本宽容不得误伤 GBK) */
|
||||||
|
const uint8_t gbk[] = { 0xC4, 0xE3, 0xBA, 0xC3 };
|
||||||
|
CHECK(rd_enc_detect(gbk, sizeof gbk, NULL) == RD_ENC_GB18030, "t13 gbk still gb18030");
|
||||||
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
{
|
{
|
||||||
t1(); t2(); t3(); t4(); t5();
|
t1(); t2(); t3(); t4(); t5();
|
||||||
t6(); t7(); t8(); t9(); t10(); t11(); t12();
|
t6(); t7(); t8(); t9(); t10(); t11(); t12(); t13();
|
||||||
printf("test_reader_core: %d pass, %d fail\n", g_pass, g_fail);
|
printf("test_reader_core: %d pass, %d fail\n", g_pass, g_fail);
|
||||||
return g_fail ? 1 : 0;
|
return g_fail ? 1 : 0;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user