公开 SDK 同步开发线:SDK 加深层改进 + 图库(picbrowser)参考应用
SDK 侧(56 文件,+4517/−116): - 内置 mipsel o32 时钟/睡眠 ABI 修复(meow_ui_time.c + CLOCK_ABI.md + 宿主单测) 不修则 zig musl 交叉编出的程序 time/gettimeofday/localtime_r 返回垃圾、usleep 空转 - 图片解码真缩解后端:JPEG DCT 域 / PNG 逐行 / BMP 跳行(meow_jpeg / meow_png_stream) - 图片层预算解码入口(峰值 43.7→14.0 MiB) - 修 meow_scroll_wrap 池槽泄漏(free 归还 wrap 上下文槽)+ 64 次回收/交错/池满回归 - 宿主测试头注释按实测修订 新增图库参考应用 tuku/(55 文件,图库/阅读器/锁屏/设置): - 书库浏览(文件管理器模型)、CBZ/ZIP 容器读取、条漫模式与续读 - 阅读器跟手翻页/预解码窗口/两级清晰度/RGB565 页槽常驻(页图内存减半) - 启动 PIN 锁屏(页内自绘键盘)、设置页 - 21 个宿主测试 + 4 个 runner:zip 106/0、cbz 296/0、webtoon、reader 内存预算全通过 已验证:干净克隆下 sdk/build_sdk.sh 产出 34 成员 MIPS32r2 meow_ui.a; tuku/build_tuku.sh 产出 562KB 静态 MIPS32r2 ELF;全部宿主测试通过。
This commit is contained in:
commit
38cc9d4acb
114 files changed
+22113
-117
No files matched your search
+12
@@ -44,6 +44,18 @@ test_umbrella
|
||||
examples/hello/hello
|
||||
examples/hello/render_test*.png
|
||||
|
||||
# ---- 图库产物 ----
|
||||
tuku/picbrowser
|
||||
tuku/*.mpk
|
||||
tuku/build/
|
||||
tuku/test/test_library
|
||||
tuku/test/test_reader_core
|
||||
tuku/test/test_settings_reader_persist
|
||||
tuku/test/test_settings_lock_host
|
||||
tuku/test/test_pin
|
||||
tuku/test/test_zip
|
||||
tuku/test/test_cbz
|
||||
|
||||
# ---- 杂项 ----
|
||||
*.orig
|
||||
*.log
|
||||
|
||||
@@ -31,6 +31,12 @@ MeowOS-SDK/
|
||||
│ ├── ui/ # 每页一个 view 文件(home/demo/list/full/chrome)
|
||||
│ ├── logic/ # 纯 C 业务逻辑(不依赖 meow_ui.h,宿主可裸测)
|
||||
│ └── build_hello.sh # 构建 hello(链 meow_ui.a + libz.a + -lpthread)
|
||||
├── tuku/ # 图库:漫画书库 App(M3 骨架:主题同步/目录浏览/阅读器桩)
|
||||
│ ├── app.c # main + 装配 + meow_ui_run 回调路由 + 主题监听
|
||||
│ ├── logic/ # 纯 C:目录扫描/自然排序、阅读方向状态(宿主可裸测)
|
||||
│ ├── ui/ # library / reader / settings / lock 每页一个 view
|
||||
│ ├── build_tuku.sh # 交叉构建(SDK 用 ${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk})
|
||||
│ └── pack_tuku.sh # 出包 os.meow.picbrowser.mpk
|
||||
└── docs/
|
||||
├── APP_DEV_GUIDE.md # 第三方开发上手指南(环境 → 最小 App → 构建打包 → 签名安装 → 调试)
|
||||
└── MEOW_APP_SDK.md # SDK 设计全量规范
|
||||
@@ -46,6 +52,11 @@ cd sdk
|
||||
# 2) 构建示例 App hello
|
||||
cd ../examples/hello
|
||||
./build_hello.sh # 产出 examples/hello/hello(静态 MIPS32r2 ELF)
|
||||
|
||||
# 3) 构建图库(SDK 用绝对路径,可用 MEOW_SDK_DIR 覆盖)
|
||||
cd ../../tuku
|
||||
./build_tuku.sh # 产出 tuku/tuku(静态 MIPS32r2 ELF)
|
||||
./pack_tuku.sh # 产出 tuku/os.meow.picbrowser.mpk
|
||||
```
|
||||
|
||||
## SDK 能力速览
|
||||
|
||||
@@ -59,7 +59,7 @@ MEOW_SYSROOT="$HOME/Project/R3ProII/.tools/mipsel-linux-muslsf-cross/mipsel-linu
|
||||
SDK 依赖板载 `/dev/fb0` 与字库,宿主机没有——但你可以**链真实 SDK 目标文件 + 打桩**来跑逻辑单测(参考 `sdk/test_host_stub.c`):
|
||||
|
||||
- `test_host_stub.c` 提供了 `meow_font_*` / `meow_ui_global_render` 等宿主桩(无 fb → 5×7 点阵回退;无字库 → `meow_font_load` 返回 -1)。
|
||||
- 宿主可 `gcc` 直接编译 SDK 各 `.c` + 你的逻辑,跑纯 C 断言,**不碰硬件**(`hello` 的 `test_p1p2p3_repro` / `test_meow_ui_widgets` / `test_pinyin` 等都是这么跑的,宿主宿主验证全 PASS)。
|
||||
- 宿主可 `gcc` 直接编译 SDK 各 `.c` + 你的逻辑,跑纯 C 断言,**不碰硬件**(`hello` 的 `test_p1p2p3_repro` / `test_meow_ui_widgets` / `test_pinyin` 等都是这么跑的)。注意:公开仓 38 个 `sdk/test_*.c` 的头注释已按实测修订——按注释补齐 `-Iinclude`、`meow_ui_nav.c` 等源集后 **34/38 全部 PASS**;`test_notify_post` / `test_ui_audio_host` / `test_p1p2p3_repro` 依赖主仓组件(launcher/audiod/hello_app),公开仓跳过;`test_net_ipc` 需运行中的 netmgrd,属手工验证项。宿主 `/tmp` 需可写(Termux 本机可用 `proot -b <scratch>:/tmp` 提供)。
|
||||
- 注意:`logic/` 层要写**纯 C**(不依赖 `meow_ui.h`),才能裸宿主单测;`ui/` 层才碰 SDK。
|
||||
|
||||
### 1.3 qemu-mipsel 全系统仿真
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
# SDK 时钟 ABI 说明(meow_ui_time_ms 真机故障的根因与修复)
|
||||
|
||||
面向对象:维护 SDK / 工具链 / 重链 App 的人。**结论先行**:这不是 SDK 代码写错,
|
||||
是 zig 0.17 自带 musl 在 mipsel 上的 time64 ABI 自相矛盾;修复落在 SDK 链上
|
||||
(`patch_v2/sdk/meow_ui_time.c` + 链接期强定义),改后需重编 `meow_ui.a` 并重链四 App。
|
||||
|
||||
## 1. 现象(真机)
|
||||
|
||||
M10 抓到的日志首列(`meow_ui_time_ms()`,64 位)可拆成:
|
||||
|
||||
```
|
||||
6607363222717682072 → lo=1135000 hi=1538396632
|
||||
8754846870717682072 → lo=1135000 hi=2038396632
|
||||
18138485921460348192 → lo=1140000 hi=4223195352
|
||||
```
|
||||
|
||||
- 低 32 位 = 开机秒数 × 1000,**恒为 1000 的倍数 → 毫秒位丢失**;
|
||||
- 高 32 位每次调用随机、无单调性。
|
||||
|
||||
同进程另一份取时(旧容器编的 core)正常 → 问题只在「zig 0.17 编的那部分」。
|
||||
|
||||
## 2. 根因链(三段证据)
|
||||
|
||||
### 2.1 zig 头文件:`time_t` 是 64 位,`struct timespec` 16 字节
|
||||
|
||||
`<zig lib>/libc/include/mips-linux-musl/bits/alltypes.h`:
|
||||
|
||||
```c
|
||||
#define _REDIR_TIME64 1
|
||||
#define __USE_TIME_BITS64 1
|
||||
typedef _Int64 time_t; /* 无条件 64 位 */
|
||||
struct timespec { time_t tv_sec; ...; long tv_nsec; ... }; /* tv_sec@0(8B)、tv_nsec@8 */
|
||||
```
|
||||
|
||||
`include/generic-musl/time.h` 因此在 `_REDIR_TIME64` 下把 `clock_gettime` 重定向到
|
||||
`__clock_gettime64`。用该链编译的 C 代码一律按「16 字节、tv_sec 8 字节」取数
|
||||
(`patch_v2/sdk/meow_ui.a(meow_ui_gfx.o)` 的旧 `meow_ui_time_ms` 反汇编:`lw 0x10/0x14`
|
||||
取 tv_sec、`lw 0x18` 取 tv_nsec,正是这个布局)。
|
||||
|
||||
### 2.2 zig 的 clang 给 mipsel 预定义了 `__ILP32__`
|
||||
|
||||
```
|
||||
$ echo | zig cc --target=mipsel-linux-musleabi -dM -E - | grep __ILP32__
|
||||
#define __ILP32__ 1
|
||||
```
|
||||
|
||||
(arm-linux-musleabi 同样为 1;x86-linux-musl / aarch64 为 0。容器里的 gcc 对 mips o32
|
||||
**不定义** `__ILP32__`。)
|
||||
|
||||
### 2.3 musl 的 time64 回退分支被这句 `!__ILP32__` 整体编译掉
|
||||
|
||||
`<zig lib>/libc/musl/src/time/clock_gettime.c`:
|
||||
|
||||
```c
|
||||
#if defined(SYS_clock_gettime64) && !defined(__ILP32__)
|
||||
... __syscall(SYS_clock_gettime64, clk, ts); /* 4403 */
|
||||
... 失败(ENOSYS) 再 __syscall(SYS_clock_gettime, clk, ts32); /* 4263 + 宽度转换 */
|
||||
#else
|
||||
r = __syscall(SYS_clock_gettime, clk, ts); /* 直接把 8 字节 time32 写进调用者缓冲 */
|
||||
#endif
|
||||
```
|
||||
|
||||
这句 `!__ILP32__` 本意只排除 x32(`__ILP32__` 是 x32 的标记),但 clang 把它当成
|
||||
「32 位数据模型」的通名。于是 zig 的 mipsel libc 走进 `#else`:
|
||||
`__clock_gettime64` 实际执行 **SYS_clock_gettime(4263)**,只写 8 字节
|
||||
`{ int32 tv_sec; int32 tv_nsec; }`。
|
||||
|
||||
**证据(反汇编,当前 zig 0.17 静态链接的探针)**:
|
||||
|
||||
```
|
||||
0002576c <__clock_gettime64>:
|
||||
257c8: addiu $2,$zero,0x10a7 ; 4263 = SYS_clock_gettime(time32)
|
||||
257cc: syscall ; $5 直接是调用者的 16 字节 struct 指针,无 4403 尝试、无转换
|
||||
```
|
||||
|
||||
同一符号在**旧容器产物** `patch_v2/ncm_app/ncm`(已入库,gcc 构建)里是:
|
||||
|
||||
```
|
||||
115748: addiu $2,$zero,0x1133 ; 4403 = SYS_clock_gettime64
|
||||
11574c: syscall
|
||||
...
|
||||
115774: addiu $2,$zero,0x10a7 ; ENOSYS 后回退 4263,写入本地 ts32
|
||||
...
|
||||
; 再把 ts32 升成 64 位写回调用者(tv_sec=tv_sec、tv_nsec=tv_nsec)
|
||||
```
|
||||
|
||||
两者对比即根因:**头文件 ABI(64 位 time_t)与 libc 实现 ABI(32 位 time32)不一致**。
|
||||
|
||||
### 2.4 数值解释(为什么「低 32 = 秒×1000、高 32 随机」)
|
||||
|
||||
libc 把 `{sec, nsec}` 当两个 int32 写进 16 字节缓冲的前 8 字节;SDK 按 64 位读:
|
||||
|
||||
```
|
||||
tv_sec = sec | (nsec << 32) // 低字正确、高字=纳秒
|
||||
tv_nsec = 缓冲偏移 8 的未初始化值 // 毫秒项≈0
|
||||
result = tv_sec*1000 + tv_nsec/1e6
|
||||
= sec*1000 + (nsec*1000 mod 2^32)<<32 + 垃圾/1e6
|
||||
```
|
||||
|
||||
- 低 32 位只剩 `sec*1000`(nsec 项左移 32 位不落到低字)→ 恒为 1000 的倍数;
|
||||
- 高 32 位 = `nsec*1000 mod 2^32` → 每次随机、不单调;
|
||||
- `tv_nsec` 来自越界读 → 通常是小值/0(真机上恰好 0,所以没有额外尾数)。
|
||||
|
||||
## 3. 修复(`patch_v2/sdk/meow_ui_time.c`)
|
||||
|
||||
1. **取数走内核 o32 ABI**:`meow_ui_time_ms()` 用
|
||||
`syscall(SYS_clock_gettime32, CLOCK_MONOTONIC, &{int32 sec; int32 nsec;})`,
|
||||
再 `meow_time_ms_from_parts(sec, nsec)` 升成 64 位毫秒。完全不依赖 libc 头声明的
|
||||
`struct timespec` 宽度 —— 即使将来 zig 修好(或换回容器链),内核 time32 入口始终存在。
|
||||
2. **链接期修 libc 入口**:同一 TU 强定义 `__clock_gettime` / `__clock_gettime64`
|
||||
(musl 的 `time()`、`gettimeofday()`、`__timedwait_cp()`(pthread 定时等待)内部都调
|
||||
`__clock_gettime`)。应用链接顺序是 `... meow_ui.a ... -lc`,归档先解析符号,libc 里那份
|
||||
坏实现(及其 VDSO 前导)不会被拉入 —— 用 `nm` 验证链接产物里没有 `cgt_init/__vdsosym`,
|
||||
且 `__time64`/`__timedwait_cp` 的 `jal` 指向本 TU 的实现。
|
||||
3. **睡眠入口一并修正(同源缺陷)**:`__nanosleep_time64` / `__clock_nanosleep(_time64)`
|
||||
先按 time32 转换再发 `SYS_nanosleep`/`SYS_clock_nanosleep`,`rem` 再扩回 64 位。
|
||||
修复前 zig libc 把 16 字节 time64 结构直接交给 time32 系统调用,内核读到的 nsec 恒为 0 →
|
||||
SDK 里 `usleep(16000)`(帧节拍、输入轮询、IME 冷却)全部变成忙等空转。
|
||||
4. **去掉四处重复的 `now_ms()`**:`meow_ui_gfx.c` / `meow_ui_sys.c` / `meow_ui_input.c` /
|
||||
`meow_cachegov.c` 统一调 `meow_ui_time_ms()`(cachegov 保留宿主测试注入桩)。
|
||||
5. 构建脚本 `build_sdk.sh` 的 OBJS 增加 `meow_ui_time`;宿主单测 `test_meow_time.c`
|
||||
固定换算边界与「单调且前进」;探针 `tools/clock_abi_probe.c` 用于宿主/真机一眼验收。
|
||||
|
||||
非 mips 目标(宿主单测、aarch64 宿主)走普通 `clock_gettime`,行为不变。
|
||||
|
||||
## 4. 验证
|
||||
|
||||
宿主:
|
||||
|
||||
```
|
||||
cd patch_v2/sdk
|
||||
gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o /tmp/meow_ui_time.o
|
||||
gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c /tmp/meow_ui_time.o -o /tmp/test_meow_time
|
||||
/tmp/test_meow_time # == PASS ==(换算边界 + 单调前进)
|
||||
```
|
||||
|
||||
交叉:
|
||||
|
||||
```
|
||||
bash patch_v2/sdk/build_sdk_host.sh --verify # 成员清单/架构自检 + 0 warning
|
||||
llvm-objdump -d meow_ui.a(meow_ui_time.o) # meow_ui_time_ms: syscall 4263, lw 0x18 取 nsec、sra 扩展 tv_sec
|
||||
```
|
||||
|
||||
链接期接管(用真实归档链一个探针,检查符号来源):
|
||||
|
||||
```
|
||||
# 不应出现 libc 的 VDSO 前导(cgt_init/__vdsosym)→ libc 的取时实现未被拉入
|
||||
strings -a clock_abi_probe_sdk | grep -c __vdso # 期望 0
|
||||
llvm-nm clock_abi_probe_sdk | grep -E 'clock_gettime|nanosleep'
|
||||
# __time64 / __timedwait_cp 的 jal 应指向本 TU 的 __clock_gettime;睡眠走 SYS_nanosleep(4166)/clock_nanosleep(4265)
|
||||
```
|
||||
|
||||
探针 `tools/clock_abi_probe.c`(宿主 `gcc` 或 mipsel 交叉;加 `-DMEOW_PROBE_WITH_SDK`
|
||||
并链 `meow_ui.a` 可同时验收 `meow_ui_time_ms`):把 `struct timespec` 预填 0xAA 后取时,
|
||||
若偏移 8..11 仍是 0xAA 即说明 libc 只写了 8 字节(ABI 不一致),退出码 2。
|
||||
|
||||
真机(塔台批准后):日志首列应连续、出现非整千差;`meow_ui_time_ms()` 两次取样差 = 真实毫秒。
|
||||
本次设备已离线,真机确认**待办**。
|
||||
|
||||
## 5. 影响面
|
||||
|
||||
- **SDK 内**:`meow_ui_time_ms()` 及其上层 —— 动画 tween(`meow_ui_anim.c`)、滚动/fling
|
||||
(`meow_ui_scroll.c`/`meow_select.c`)、IME 冷却(`meow_ui_ime.c`)、状态栏动画
|
||||
(`meow_ui_gfx.c`)、通知 TTL(`meow_ui_notify.c`)、输入层 tick/启动冷却
|
||||
(`meow_ui_input.c`)、cachegov 回退窗口(`meow_cachegov.c`)、日志首列(`meow_ui_log.c`)。
|
||||
- **App 侧**:`on_tick` 入参 `now`、`meow_anim_*`、以及任何自行调用 `meow_ui_time_ms()` 的
|
||||
App(hello/reader/files/ncm…)。
|
||||
- **产物**:`meow_ui.a` 变更 → 需**重编并重链四 App**(ncm/reader/files/hello);
|
||||
`time()`/`gettimeofday()` 的修正也在同一份归档里生效(链接期覆盖)。
|
||||
- 真机写入红线未触碰;本单只改 SDK 源与构建脚本。
|
||||
|
||||
## 6. 同源残余(未在本单修复,需单独安排)
|
||||
|
||||
同一句 `!__ILP32__` 还打穿了以下路径(睡眠族已在上文一并修掉,余下这些症状不同、
|
||||
修复面更大):
|
||||
|
||||
| 路径 | 症状 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `futex` 相对超时 | 亚秒 timed wait 立即超时 | `__timedwait_cp` 的 `__futex4_cp` 同款 `!__ILP32__` 守卫;`pthread_cond_timedwait` 的「now」已由本单修正,但相对超时仍按 8 字节读(>1s 的整秒等待基本可用,亚秒等待失效) |
|
||||
| `timerfd_settime` | 到期时间错乱 | `struct itimerspec`(32 字节)当 16 字节读;当前 SDK 未用 timerfd |
|
||||
| `clock_settime`/`clock_adjtime`/`pselect`/`ppoll`/`sigtimedwait`/`semtimedop` 等 | 未验证 | 同族守卫;SDK 目前未直接使用 |
|
||||
|
||||
建议:向 zig 上游报 musl `__ILP32__` 守卫问题(clang 对 32 位目标统一定义 `__ILP32__`,
|
||||
而该守卫本意只排除 x32);根治应换用容器旧链(gcc,不定义 `__ILP32__`)编 SDK,或
|
||||
downgrade/修补 zig 内置 musl。**不要**指望 `-D_TIME_BITS=32`:zig 的
|
||||
`alltypes.h` 是无条件 `#define __USE_TIME_BITS64 1`,命令行改不动;而只改用户侧头文件会让
|
||||
用户结构(8 字节)与 libc 内部(16 字节,pthread 等)再次错配。
|
||||
@@ -0,0 +1,59 @@
|
||||
# sdk/ — meow_ui 应用框架(源码 + 构建)
|
||||
|
||||
本目录是 `meow_ui.a` 的**自包含**源码树:公共头在 `include/`(伞头 `meow_ui.h`),
|
||||
构建脚本 `build_sdk.sh`,宿主逻辑测试 `test_*.c`。上手指南见仓库根
|
||||
[README.md](../README.md) 与 [docs/APP_DEV_GUIDE.md](../docs/APP_DEV_GUIDE.md)。
|
||||
|
||||
## 构建
|
||||
|
||||
```sh
|
||||
cd sdk
|
||||
./build_sdk.sh # 产出 sdk/meow_ui.a(mipsel 静态库)
|
||||
```
|
||||
|
||||
交叉编译器默认 `$HOME/Project/R3ProII/.tools/zig-mipsel-cc`,可用 `MEOW_CC` 覆盖。
|
||||
|
||||
## 时钟:mipsel o32 时钟/睡眠 ABI 修复(已内置)
|
||||
|
||||
**公开 SDK 现已内置 mipsel o32 时钟/睡眠 ABI 修复(见 [CLOCK_ABI.md](CLOCK_ABI.md))**,
|
||||
源码在 `meow_ui_time.c`。
|
||||
|
||||
- 背景:zig 内置 musl 在 mipsel 上「头文件声明 64 位 `time_t`、实现却走 time32 系统调用」,
|
||||
导致 `meow_ui_time_ms()` 毫秒位恒 0、高 32 位随机;`usleep()` 亚秒等待立即返回(忙等空转)。
|
||||
- 修复:`meow_ui_time_ms()` 直接按**内核 o32 ABI**(8 字节 `{int32 sec; int32 nsec;}`)取时,
|
||||
再自行升成 64 位毫秒;同一 TU 强定义 `__clock_gettime` / `__clock_gettime64` /
|
||||
`__clock_nanosleep` / `__clock_nanosleep_time64` / `__nanosleep_time64`,在**链接期接管**
|
||||
libc 里那份 ABI 不一致的实现。
|
||||
- **链接顺序要求**:应用必须把 `meow_ui.a` 放在 `-lc` **之前**(`cc ... meow_ui.a ... -lc`),
|
||||
强定义才能生效。仓库内 `examples/hello/build_hello.sh` 与 `tuku/build_tuku.sh` 均如此;
|
||||
自定义构建脚本请照抄这个顺序,否则取时/睡眠会静默回到坏实现。
|
||||
- 非 mipsel 目标(宿主单测、aarch64 等)走普通 `clock_gettime`,行为不变。
|
||||
|
||||
### 已知限制
|
||||
|
||||
- **2038 前有效**:内核侧仍是 time32(`tv_sec` 为 int32),秒数超出 `INT32_MAX` 即溢出;
|
||||
本修复只保证「头/实现的宽度一致」,不把内核升到 time64。
|
||||
- 同源缺陷**未修**:futex 相对超时(`pthread_cond_timedwait` 的亚秒等待)、`timerfd_settime`、
|
||||
`clock_settime`/`clock_adjtime`、`pselect`/`ppoll` 等仍受同一句 `!__ILP32__` 影响;
|
||||
清单与影响见 [CLOCK_ABI.md](CLOCK_ABI.md) §6。SDK 自身不直接使用这些路径。
|
||||
- 真机验收:本修复的证据是反汇编 + 宿主单测(本机无 qemu-mipsel),设备侧仍需实测确认。
|
||||
|
||||
## 宿主测试
|
||||
|
||||
无硬件即可跑(`gcc` 直接编译,不链 `meow_ui.a`):
|
||||
|
||||
```sh
|
||||
cd sdk
|
||||
# 时钟换算与单调性
|
||||
gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o meow_ui_time_host.o
|
||||
gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c meow_ui_time_host.o -o test_meow_time
|
||||
./test_meow_time # == PASS == / checks=N
|
||||
|
||||
# 伞头与其余模块测试(示例)
|
||||
gcc -I. -Iinclude test_umbrella.c <各 sdk 源 .c> test_host_stub.c -lpthread -o test_umbrella
|
||||
./test_umbrella
|
||||
```
|
||||
|
||||
注意:宿主编译时 `meow_ui_time.c` 走 `MEOW_TIME32_KERNEL_ABI 0` 分支(普通 `clock_gettime`),
|
||||
因此**宿主测试不能证明 mipsel o32 路径正确** —— 那条路径的正确性靠 `CLOCK_ABI.md §4` 的
|
||||
反汇编证据(`nm` 符号来源 + `syscall 4263/4265` 的 8 字节转换)。
|
||||
+8
-1
@@ -6,6 +6,11 @@
|
||||
#
|
||||
# 交叉编译器:默认 MeowOS 开发机路径 $HOME/Project/R3ProII/.tools/zig-mipsel-cc;
|
||||
# 可用环境变量 MEOW_CC 覆盖(或用你自己装好的 zig 交叉 cc,--target=mipsel-linux-musleabi soft-float)。
|
||||
#
|
||||
# 时钟(meow_ui_time):mipsel 上 zig 内置 musl 的 time64 ABI 自相矛盾(头声明 64 位 time_t、
|
||||
# 实现走 time32 取数),该 TU 按内核 o32 ABI 取时并**强定义** __clock_gettime*/睡眠入口,靠链接期
|
||||
# 接管 libc 那份坏实现。因此应用链接顺序必须是 `... meow_ui.a ... -lc`(归档在 libc 之前,
|
||||
# 现有 build_tuku.sh 即如此);根因与证据链见 sdk/CLOCK_ABI.md。
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
CC="${MEOW_CC:-$HOME/Project/R3ProII/.tools/zig-mipsel-cc}"
|
||||
@@ -16,7 +21,9 @@ rm -f meow_ui.a *.o
|
||||
# meow_font.c = 预渲染位图字体运行时(自包含:只含 meow_font.h + 标准头;最小 zlib ABI 自声明,
|
||||
# 链接时用 sysroot 的 libz.a),随 SDK 一并 vendored。新模块(box/anim/scroll/str + cachegov/lazygrid/
|
||||
# select/net_ipc)随主仓增量并入。
|
||||
OBJS="meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image meow_font meow_ui_box meow_ui_anim meow_ui_scroll meow_ui_str meow_cachegov meow_lazygrid meow_select meow_net_ipc"
|
||||
# meow_ui_time.c 必须**编进归档**且排在前面:它提供 meow_ui_time_ms(gfx 已不再实现)与
|
||||
# __clock_gettime* 强定义,归档成员随 meow_ui_time_ms 的引用被拉入,从而在 -lc 之前解析。
|
||||
OBJS="meow_ui_time meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_jpeg meow_png_stream meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image meow_font meow_ui_box meow_ui_anim meow_ui_scroll meow_ui_str meow_cachegov meow_lazygrid meow_select meow_net_ipc"
|
||||
CFLAGS="-Os -static -Iinclude -I."
|
||||
for f in $OBJS; do
|
||||
echo "== cc $f =="
|
||||
|
||||
@@ -50,6 +50,24 @@ void meow_ui_flush_region(int x, int y, int w, int h); /* 只刷新指定区 *
|
||||
/* 半透明(blend565:前景按 alpha 与背景混叠) */
|
||||
void meow_ui_fill_rect_alpha(int x, int y, int w, int h, uint32_t color, int alpha); /* 0-255 */
|
||||
|
||||
/* ---- RGB565 页图(M32)----
|
||||
* 页槽常驻 RGB565(2 字节/像素):真机 480×853 页 1599KB → 800KB。
|
||||
* 量化与 back_buf 用同一个 565 转换(逐位截断),565 屏上最终像素与 RGBA blit
|
||||
* 路径逐像素一致——不引入色偏、不会比现在多出色带。
|
||||
* 命中页(页图尺寸 == 显示尺寸)走 meow_gfx_blit_rgb565:每行一次拷贝,无重采样。
|
||||
*
|
||||
* alpha:565 页图**按不透明**处理(alpha 被忽略)。漫画页解码后 alpha 恒为 255,
|
||||
* 与旧路径等价;带透明像素的 PNG(调色板/tRNS/color type 6、或 32bpp BMP)会有可见
|
||||
* 差异:旧 RGBA 路径把透明像素混合到阅读器底色,565 路径直接画它的 RGB(通常是黑)。
|
||||
* 若将来要支持透明页,需要在上屏时按 alpha 混合(而不是在落槽时丢通道)。 */
|
||||
uint16_t meow_gfx_rgb565(uint32_t rgb); /* 24-bit RGB → 565(与 back_buf 同式) */
|
||||
void meow_gfx_rgba_to_rgb565(const uint8_t *rgba, uint16_t *out, int npx); /* 不透明像素(忽略 alpha) */
|
||||
/* 原生尺寸 blit(不透明):16bpp back_buf 上是每行一次 memcpy。 */
|
||||
void meow_gfx_blit_rgb565(int x, int y, int w, int h, const uint16_t *px);
|
||||
/* 最近邻缩放 blit(几何不符时的兜底;采样点与 meow_img_resample 同式)。 */
|
||||
void meow_gfx_blit_rgb565_scaled(int x, int y, int w, int h,
|
||||
int sw, int sh, const uint16_t *px);
|
||||
|
||||
/* ============================================================ */
|
||||
/* §7 动画/时间(B1 最小可用) */
|
||||
/* ============================================================ */
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/*
|
||||
* meow_ui_image.h — SDK 图片层(§4,B1)。
|
||||
*
|
||||
* 原 meow_ui.h 拆分的一部分(image 模块)。内部 stb_image(单头文件,静态编入 meow_ui.a)
|
||||
* 原 meow_ui.h 拆分的一部分(image 模块)。解码后端:M23 真缩解(JPEG DCT 域 / PNG 逐行 /
|
||||
* BMP 跳行,见 meow_jpeg.h)优先,回退 stb_image(单头文件,静态编入 meow_ui.a)。
|
||||
* 解 PNG/JPEG/BMP,统一转 RGBA。meow_img 为不透明句柄(前向声明见 meow_ui_types.h)。
|
||||
*/
|
||||
#ifndef MEOW_UI_IMAGE_H
|
||||
@@ -22,6 +23,60 @@ meow_img *meow_img_load_file(const char *path); /* 内部读文件 + stb 解
|
||||
meow_img *meow_img_load_mem(const void *buf, int len);
|
||||
void meow_img_free(meow_img *img);
|
||||
|
||||
/* 追加(M12)预算解码:只读文件头取原始尺寸,不解码像素(零像素内存)。成功 1,失败 0。
|
||||
* App 可用它在加载前判断「图太大」并给用户提示,而不必先撞 OOM。 */
|
||||
int meow_img_probe_file(const char *path, int *w, int *h, int *channels);
|
||||
|
||||
/* 追加(M12/M23)预算解码:解码期受长边约束,返回图最长边 ≤ maxdim,尺寸与
|
||||
* 「meow_img_load_file + meow_img_downscale(maxdim)」逐字节同口径(公式一致)。
|
||||
*
|
||||
* 实现(M23 起):先进真缩解后端(内部 meow_jpeg.c / meow_png_stream.c),解码期只算目标分辨率:
|
||||
* JPEG:DCT 域缩解(每 8×8 块只解出 N×N 像素,N 由 8·maxdim/长边 取「不欠采样」的最小值;
|
||||
* N=1..7,N≥8 交回 stb),渐进式跳过精细化扫描、只解各频带首次扫描 —— 低频带已完整,
|
||||
* 不整幅驻留系数阵列;
|
||||
* PNG :libz 逐行 inflate + 反滤波,按步长丢行丢列直写目标尺寸(gray/gray+A/RGB/RGBA/
|
||||
* 调色板/1-16bit,16bit 取高字节,tRNS 透明含 8bit 灰/RGB);
|
||||
* BMP :按目标行 seek 到源行 + 行内跳列(8bpp 调色板 / 16bpp RGB555 / 24bpp / 32bpp)。
|
||||
* 峰值只与输出尺寸相关,不随源图分辨率/压缩体积线性增长(2000×3000 页 ≤2.5MB 量级)。
|
||||
* 不支持的特性(CMYK/YCCK 4 分量 JPEG、算术编码、过订阅 Huffman 表、16bit 量化表、Adam7
|
||||
* 交错 PNG、RLE/1-4bpp BMP、几乎等尺寸)自动回退 stb 旧路径,对外语义完全不变。
|
||||
* 小图(长边 ≤ maxdim)走旧路径,与原 API 完全一致;maxdim<=0 等价旧入口。失败返回 NULL。
|
||||
*
|
||||
* 尺寸口径说明(M23 评审 P2-2):本入口按「原图 + meow_img_downscale」公式算目标尺寸;M12 旧
|
||||
* 实现经 stb 的 ceil(w/s) 缩解再精修,源尺寸不是档位整数倍时可能与本入口差 1px(实测 1999×2999:
|
||||
* 旧 320×480 vs 新 319×480)。阅读器按显示框缩放,无实际影响;PNG/BMP 无此差异。 */
|
||||
meow_img *meow_img_load_file_dim(const char *path, int maxdim);
|
||||
meow_img *meow_img_load_mem_dim(const void *buf, int len, int maxdim);
|
||||
|
||||
/* 追加(M22)缩解档位选择:取满足「长边/档位 ≥ maxdim」的最大整数档 s ∈ {1,2,4,8},
|
||||
* 保证缩解结果不欠采样。meow_img_load_file_dim 内部用的就是这个函数;调用方(阅读器)
|
||||
* 用它枚举候选档位做内存预算,避免两处各写一份而漂移。 */
|
||||
int meow_img_pick_scale(int longside, int maxdim);
|
||||
|
||||
/* 追加(M22)显式档位解码:语义与 meow_img_load_file_dim 相同(结果长边 ≤ maxdim,
|
||||
* 超了就再精修一次),只是缩解档位由调用方决定。内存吃紧时可以选更粗的档位——输出更小、
|
||||
* 峰值更低,代价是细节变软。step <= 1 等价长边约束入口;step > 8 按 8 处理。
|
||||
* M23 起:先走真缩解后端(step 折算成目标长边 ceil(长边/step),后端按该长边选 DCT 阶),
|
||||
* 峰值只与输出尺寸相关;不支持的特性回退 stb(step 原样传入)。
|
||||
* M27 起 PNG/BMP 也走同一后端入口(阅读器峰值模型已同步为「目标图 + 工作面」口径);
|
||||
* 断言与实测见 tuku/test/test_reader_membudget.c 的 B7。 */
|
||||
meow_img *meow_img_load_file_step(const char *path, int maxdim, int step);
|
||||
|
||||
/* 追加(M27):与 meow_img_load_file_step 相同,额外控制回退并回报**实际**路径:
|
||||
* allow_stb_fallback = 1:后端不支持/失败时回退 stb(与 _step 完全一致);
|
||||
* allow_stb_fallback = 0:只尝试后端,失败返回 NULL——**绝不**回落 stb。阅读器的门控
|
||||
* 按「后端峰值」放行时必须用 0,否则头部判定通过、解码中途失败的文件会静默走 stb,
|
||||
* 实测可放出 18–36MB(截断 12MP JPEG / 头-only BMP / 非法位深 PNG)。失败页由调用方
|
||||
* 按坏图占位。
|
||||
* used_backend = 1(M23 后端直出目标尺寸)/ 0(未走成后端)。NULL 时等价 _step(允许回退)。 */
|
||||
meow_img *meow_img_load_file_step_ex(const char *path, int maxdim, int step,
|
||||
int allow_stb_fallback, int *used_backend);
|
||||
|
||||
/* 追加(M22)精确重采样到 w×h(最近邻,缩小/放大都支持)。阅读器用它把「缩解档位」的
|
||||
* 结果一次性落到显示框,使命中页仍是单次原生 blit(不做每帧重采样)。同尺寸返回 NULL
|
||||
* (调用方保留原图);失败返回 NULL。 */
|
||||
meow_img *meow_img_resample(meow_img *im, int w, int h);
|
||||
|
||||
int meow_img_w(const meow_img *img);
|
||||
int meow_img_h(const meow_img *img);
|
||||
/* 访问器:取 RGBA 像素指针(4 通道 straight alpha;供取色/直读像素)。meow_img 对外 opaque,
|
||||
|
||||
+1005
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* meow_jpeg.h — M23 真缩解后端内部接口(**不对外导出**,不在 meow_ui.h 伞头里)。
|
||||
*
|
||||
* 后端直接产出「目标显示尺寸」的 RGBA,不做「先整幅解、再重采样」:
|
||||
* - JPEG(meow_jpeg.c):DCT 域缩解 1/8、1/4、1/2(N=1/2/4 点 IDCT),渐进式只解到
|
||||
* 首个精细化扫描(首个 Ah>0)为止——低频带此时已完整,再往后只是为了更细的量化精度。
|
||||
* - PNG(meow_png_stream.c):libz inflate 逐行 defilter,按步长丢行丢列直写目标尺寸。
|
||||
* - BMP(meow_png_stream.c):按行 seek + 行内跳列(同一套「只碰需要的行」思路)。
|
||||
* 三者峰值都只与「输出尺寸 + 目标图像」相关,不随源图分辨率线性增长——**前提是文件头
|
||||
* 声明的尺寸在合理范围内**:畸形/超大声明尺寸(例如 23 字节的 SOF 声明 65535×65535)会被
|
||||
* 直接拒绝(返回 0 → 回退 stb,安全失败),不会按声明尺寸分配平面。
|
||||
*
|
||||
* 不支持的特性一律返回 0,由 meow_ui_img.c 回退 stb(行为与 M23 之前完全一致):
|
||||
* 4 分量 CMYK/YCCK、算术编码/无损/分层 JPEG、Adam7 交错 PNG、1/2/4bpp 与 RLE BMP 等。
|
||||
*
|
||||
* 许可证:本文件与 meow_jpeg.c / meow_png_stream.c 为 MeowOS 自写代码(MIT,随 SDK 发布)。
|
||||
* 不引入第三方解码器源码——理由见 M23 报告「为什么不用 IJG libjpeg」:libjpeg 系列对
|
||||
* 渐进式 JPEG 必须整幅驻留系数阵列(≈1 字节/源像素/分量),正是审计里 +23.5MB 的来源,
|
||||
* 换成它也无法把渐进式峰值压到 2.5MB 以内。
|
||||
*/
|
||||
#ifndef MEOW_JPEG_H
|
||||
#define MEOW_JPEG_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 解码 path / 内存里的图,直接产出「长边 ≤ maxdim 且按 meow_img_downscale 同一公式等比缩放」
|
||||
* 的 tw×th RGBA(4 通道 straight alpha,malloc 分配,调用方 free)。
|
||||
* 返回 1 成功(tw/th/rgba 已填);0 = 不支持/失败/不需要(调用方回退 stb 或旧入口)。 */
|
||||
int meow_img_backend_file(const char *path, int maxdim, int *tw, int *th, unsigned char **rgba);
|
||||
int meow_img_backend_mem(const void *buf, size_t len, int maxdim, int *tw, int *th, unsigned char **rgba);
|
||||
|
||||
/* 目标尺寸:与 meow_img_downscale 同一公式(长边 = maxdim,短边按比例截断,下限 1)。 */
|
||||
void meow_img_target_dims(int w, int h, int maxdim, int *tw, int *th);
|
||||
|
||||
/* meow_png_stream.c 提供(PNG 逐行 inflate / BMP 按行跳采样)。f 与 mem 二选一:
|
||||
* f != NULL 走文件流(只读需要的行/块),否则从 mem[0..len) 读。 */
|
||||
int meow_png_decode_stream(FILE *f, const unsigned char *mem, size_t len,
|
||||
int maxdim, int *tw, int *th, unsigned char **rgba);
|
||||
int meow_bmp_decode_stream(FILE *f, const unsigned char *mem, size_t len,
|
||||
int maxdim, int *tw, int *th, unsigned char **rgba);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MEOW_JPEG_H */
|
||||
@@ -0,0 +1,499 @@
|
||||
/*
|
||||
* meow_png_stream.c — M23 真缩解后端(二):PNG 逐行 inflate + BMP 按行跳采样。
|
||||
*
|
||||
* 为什么不用 stb 的 PNG 路径:stb 先整幅 inflate + 反滤波成 raw,再展开成 RGBA 整幅,
|
||||
* 最后才降采样——1600×2400 PNG 实测 +26.2MB。这里改成「按行流式」:
|
||||
* - IDAT 边到边 inflate,一次只持有一行(filter 字节 + rowbytes)与上一行;
|
||||
* - 反滤波后**只把需要的列**写进目标行(最近邻,与 meow_img_downscale 同口径);
|
||||
* - 不需要的行整行丢弃(但仍要反滤波,filter 依赖上一行);
|
||||
* - 峰值 ≈ 两行缓冲 + zlib 窗口(32KB) + 目标 RGBA,与源图分辨率无关。
|
||||
* 支持:gray / gray+A / RGB / RGBA / 调色板,bit depth 1/2/4/8/16(16bit 取高字节降 8bit),
|
||||
* tRNS 透明。不支持 → 返回 0 回退 stb:Adam7 交错、非 0 压缩/滤波方法、异常色型组合。
|
||||
*
|
||||
* BMP:文件流按目标行直接 seek 到源行(不读全图),行内跳列采样。支持 8bpp 调色板 /
|
||||
* 16bpp RGB555 / 24bpp / 32bpp(BI_RGB);RLE、BITFIELDS、1/2/4bpp → 回退 stb。
|
||||
*
|
||||
* 许可证:MeowOS 自写(MIT,随 SDK 发布)。PNG/zlib 语义参考 RFC 2083 / zlib 手册;
|
||||
* BMP 布局参考 stb_image 的解析口径(同为本仓已有依赖,行为对齐)。
|
||||
*/
|
||||
#include "meow_jpeg.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---------------- zlib ABI ---------------- */
|
||||
#if defined(__has_include)
|
||||
# if __has_include(<zlib.h>)
|
||||
# include <zlib.h>
|
||||
# define MZP_HAVE_H 1
|
||||
# endif
|
||||
#endif
|
||||
#ifndef MZP_HAVE_H
|
||||
/* 设备 musl sysroot 没有 zlib.h(meow_font.c 同此口径):按 libz.a 的 ABI 声明最小 API。
|
||||
* 指针/unsigned long 交给编译器定尺寸,宿主 64 位与设备 32 位都成立。 */
|
||||
typedef unsigned char mzp_Bytef;
|
||||
typedef unsigned int mzp_uInt;
|
||||
typedef unsigned long mzp_uLong;
|
||||
typedef void *mzp_voidpf;
|
||||
typedef struct mzp_stream_s {
|
||||
mzp_Bytef *next_in; mzp_uInt avail_in; mzp_uLong total_in;
|
||||
mzp_Bytef *next_out; mzp_uInt avail_out; mzp_uLong total_out;
|
||||
char *msg;
|
||||
void *state;
|
||||
void *zalloc;
|
||||
void *zfree;
|
||||
mzp_voidpf opaque;
|
||||
int data_type;
|
||||
mzp_uLong adler;
|
||||
mzp_uLong reserved;
|
||||
} mzp_stream;
|
||||
extern int inflateInit_(mzp_stream *, const char *, int);
|
||||
extern int inflate(mzp_stream *, int);
|
||||
extern int inflateEnd(mzp_stream *);
|
||||
extern const char *zlibVersion(void);
|
||||
# define z_stream mzp_stream
|
||||
# define Z_OK 0
|
||||
# define Z_STREAM_END 1
|
||||
# define Z_NO_FLUSH 0
|
||||
#endif
|
||||
|
||||
/* ---------------- 字节源 ---------------- */
|
||||
typedef struct {
|
||||
FILE *f;
|
||||
const unsigned char *mem;
|
||||
size_t memlen, mempos;
|
||||
} png_src;
|
||||
|
||||
static int ps_read(png_src *s, void *dst, size_t n)
|
||||
{
|
||||
if (s->mem) {
|
||||
if (s->mempos + n > s->memlen) return 0;
|
||||
memcpy(dst, s->mem + s->mempos, n);
|
||||
s->mempos += n;
|
||||
return 1;
|
||||
}
|
||||
return fread(dst, 1, n, s->f) == n;
|
||||
}
|
||||
|
||||
static int ps_skip(png_src *s, size_t n)
|
||||
{
|
||||
if (s->mem) {
|
||||
if (s->mempos + n > s->memlen) return 0;
|
||||
s->mempos += n;
|
||||
return 1;
|
||||
}
|
||||
return fseek(s->f, (long)n, SEEK_CUR) == 0;
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* PNG */
|
||||
/* ================================================================== */
|
||||
typedef struct {
|
||||
png_src src;
|
||||
z_stream z;
|
||||
int z_ready, z_end;
|
||||
unsigned w, h;
|
||||
int depth, color;
|
||||
int bits; /* 每像素位数 */
|
||||
size_t rowbytes;
|
||||
unsigned char *line; /* 1 filter 字节 + rowbytes */
|
||||
unsigned char *prev; /* 上一行的像素字节 */
|
||||
unsigned char *cur; /* 本行的像素字节 */
|
||||
size_t need, got;
|
||||
unsigned char pal[256][3];
|
||||
unsigned char trns_a[256]; /* 调色板透明度 */
|
||||
int pal_n;
|
||||
int have_trns;
|
||||
unsigned trns[3]; /* gray / RGB 的 tRNS 值(已降到 8bit) */
|
||||
unsigned char *out;
|
||||
int tw, th;
|
||||
int next_ty;
|
||||
unsigned y;
|
||||
} png_ctx;
|
||||
|
||||
/* 从行里取 px 列的 8bit 通道;返回通道数(写入 o[0..3] 的 RGB(A) 语义由调用方定)。 */
|
||||
static void png_get_px(const png_ctx *c, const unsigned char *row, int x, unsigned char *o)
|
||||
{
|
||||
int d = c->depth;
|
||||
if (d < 8) {
|
||||
int per = 8 / d; /* 每字节像素数 */
|
||||
int byte = x / per;
|
||||
int shift = 8 - d * (x % per + 1);
|
||||
int v = (row[byte] >> shift) & ((1 << d) - 1);
|
||||
if (c->color == 3) { /* 调色板:子字节值即索引 */
|
||||
o[0] = c->pal[v][0]; o[1] = c->pal[v][1]; o[2] = c->pal[v][2];
|
||||
o[3] = c->trns_a[v];
|
||||
} else {
|
||||
unsigned char g = (unsigned char)(v * 255 / ((1 << d) - 1));
|
||||
o[0] = o[1] = o[2] = g;
|
||||
/* tRNS 存的是原始采样值(0..2^d-1),不是展开后的 0..255 */
|
||||
o[3] = (c->color == 0 && c->have_trns && (unsigned)v == c->trns[0]) ? 0 : 255;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (d == 8) {
|
||||
if (c->color == 0) { /* gray */
|
||||
o[0] = o[1] = o[2] = row[x];
|
||||
o[3] = (c->have_trns && row[x] == c->trns[0]) ? 0 : 255;
|
||||
} else if (c->color == 2) { /* RGB */
|
||||
o[0] = row[x * 3]; o[1] = row[x * 3 + 1]; o[2] = row[x * 3 + 2];
|
||||
o[3] = (c->have_trns && row[x * 3] == c->trns[0] &&
|
||||
row[x * 3 + 1] == c->trns[1] && row[x * 3 + 2] == c->trns[2]) ? 0 : 255;
|
||||
} else if (c->color == 3) { /* palette */
|
||||
int i = row[x];
|
||||
o[0] = c->pal[i][0]; o[1] = c->pal[i][1]; o[2] = c->pal[i][2];
|
||||
o[3] = c->trns_a[i];
|
||||
} else if (c->color == 4) { /* gray + A */
|
||||
o[0] = o[1] = o[2] = row[x * 2];
|
||||
o[3] = row[x * 2 + 1];
|
||||
} else { /* RGBA */
|
||||
o[0] = row[x * 4]; o[1] = row[x * 4 + 1];
|
||||
o[2] = row[x * 4 + 2]; o[3] = row[x * 4 + 3];
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* 16bit:取高字节 */
|
||||
if (c->color == 0) {
|
||||
o[0] = o[1] = o[2] = row[x * 2];
|
||||
o[3] = (c->have_trns && row[x * 2] == c->trns[0]) ? 0 : 255;
|
||||
} else if (c->color == 2) {
|
||||
o[0] = row[x * 6]; o[1] = row[x * 6 + 2]; o[2] = row[x * 6 + 4];
|
||||
o[3] = (c->have_trns && row[x * 6] == c->trns[0] &&
|
||||
row[x * 6 + 2] == c->trns[1] && row[x * 6 + 4] == c->trns[2]) ? 0 : 255;
|
||||
} else if (c->color == 4) {
|
||||
o[0] = o[1] = o[2] = row[x * 4];
|
||||
o[3] = row[x * 4 + 2];
|
||||
} else {
|
||||
o[0] = row[x * 8]; o[1] = row[x * 8 + 2];
|
||||
o[2] = row[x * 8 + 4]; o[3] = row[x * 8 + 6];
|
||||
}
|
||||
}
|
||||
|
||||
/* 一行反滤波完成后:按需写目标行。 */
|
||||
static int png_emit_row(png_ctx *c)
|
||||
{
|
||||
unsigned char ft = c->line[0];
|
||||
const unsigned char *src = c->line + 1;
|
||||
unsigned char *dst = c->cur;
|
||||
size_t x, bpp = (size_t)((c->bits + 7) / 8);
|
||||
if (bpp < 1) bpp = 1;
|
||||
if (ft > 4) return 0;
|
||||
for (x = 0; x < c->rowbytes; x++) {
|
||||
unsigned char left = (x >= bpp) ? dst[x - bpp] : 0;
|
||||
unsigned char up = c->prev ? c->prev[x] : 0;
|
||||
unsigned char ul = (c->prev && x >= bpp) ? c->prev[x - bpp] : 0;
|
||||
unsigned char v = src[x];
|
||||
switch (ft) {
|
||||
case 0: dst[x] = v; break;
|
||||
case 1: dst[x] = (unsigned char)(v + left); break;
|
||||
case 2: dst[x] = (unsigned char)(v + up); break;
|
||||
case 3: dst[x] = (unsigned char)(v + (((unsigned)left + (unsigned)up) >> 1)); break;
|
||||
default: {
|
||||
int p = (int)left + (int)up - (int)ul;
|
||||
int pa = p - (int)left; if (pa < 0) pa = -pa;
|
||||
int pb = p - (int)up; if (pb < 0) pb = -pb;
|
||||
int pc = p - (int)ul; if (pc < 0) pc = -pc;
|
||||
unsigned char pred = (pa <= pb && pa <= pc) ? left : (pb <= pc ? up : ul);
|
||||
dst[x] = (unsigned char)(v + pred);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (c->next_ty < c->th) {
|
||||
long sy = (long)c->next_ty * (long)c->h / c->th;
|
||||
if (sy == (long)c->y) {
|
||||
unsigned char *o = c->out + (size_t)c->next_ty * c->tw * 4;
|
||||
int tx;
|
||||
for (tx = 0; tx < c->tw; tx++) {
|
||||
int sx = (int)((long)tx * (long)c->w / c->tw);
|
||||
unsigned char px[4];
|
||||
png_get_px(c, dst, sx, px);
|
||||
o[tx * 4] = px[0]; o[tx * 4 + 1] = px[1];
|
||||
o[tx * 4 + 2] = px[2]; o[tx * 4 + 3] = px[3];
|
||||
}
|
||||
c->next_ty++;
|
||||
}
|
||||
}
|
||||
/* 交换 prev/cur(prev 直接成为反滤波基准) */
|
||||
{
|
||||
unsigned char *t = c->prev;
|
||||
c->prev = c->cur;
|
||||
c->cur = t;
|
||||
}
|
||||
c->y++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
#define PNG_NEED_MORE 2
|
||||
|
||||
static int png_pump(png_ctx *c)
|
||||
{
|
||||
while (c->got < c->need) {
|
||||
int ret;
|
||||
c->z.next_out = c->line + c->got;
|
||||
c->z.avail_out = (unsigned)(c->need - c->got);
|
||||
ret = inflate(&c->z, Z_NO_FLUSH);
|
||||
c->got = c->need - c->z.avail_out;
|
||||
if (ret == Z_STREAM_END) c->z_end = 1;
|
||||
else if (ret != Z_OK) return 0;
|
||||
if (c->got < c->need && (c->z.avail_in == 0 || c->z_end))
|
||||
return c->z_end ? 0 : PNG_NEED_MORE;
|
||||
}
|
||||
if (!png_emit_row(c)) return 0;
|
||||
c->got = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int png_bits_of(int color, int depth)
|
||||
{
|
||||
switch (color) {
|
||||
case 0: return depth; /* gray */
|
||||
case 2: return depth * 3; /* rgb */
|
||||
case 3: return depth; /* palette */
|
||||
case 4: return depth * 2; /* gray + a */
|
||||
case 6: return depth * 4; /* rgba */
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int png_depth_ok(int color, int depth)
|
||||
{
|
||||
if (color == 0) return depth == 1 || depth == 2 || depth == 4 || depth == 8 || depth == 16;
|
||||
if (color == 3) return depth == 1 || depth == 2 || depth == 4 || depth == 8;
|
||||
if (color == 2 || color == 4 || color == 6) return depth == 8 || depth == 16;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int meow_png_decode_stream(FILE *f, const unsigned char *mem, size_t len,
|
||||
int maxdim, int *tw, int *th, unsigned char **rgba)
|
||||
{
|
||||
png_ctx c;
|
||||
unsigned char sig[8], hdr[8];
|
||||
unsigned char ibuf[4096];
|
||||
int have_ihdr = 0, i, ok = 0;
|
||||
|
||||
if (maxdim <= 0) return 0;
|
||||
memset(&c, 0, sizeof c);
|
||||
c.src.f = f;
|
||||
c.src.mem = mem;
|
||||
c.src.memlen = len;
|
||||
if (!ps_read(&c.src, sig, 8)) return 0;
|
||||
if (memcmp(sig, "\x89PNG\r\n\x1a\n", 8) != 0) return 0;
|
||||
|
||||
while (ps_read(&c.src, hdr, 8)) {
|
||||
unsigned clen = ((unsigned)hdr[0] << 24) | ((unsigned)hdr[1] << 16) |
|
||||
((unsigned)hdr[2] << 8) | hdr[3];
|
||||
const unsigned char *type = hdr + 4;
|
||||
if (clen > 0x7FFFFFFFu) return 0;
|
||||
|
||||
if (memcmp(type, "IHDR", 4) == 0) {
|
||||
unsigned char d[13];
|
||||
if (clen < 13 || have_ihdr) return 0;
|
||||
if (!ps_read(&c.src, d, 13)) return 0;
|
||||
c.w = ((unsigned)d[0] << 24) | ((unsigned)d[1] << 16) | ((unsigned)d[2] << 8) | d[3];
|
||||
c.h = ((unsigned)d[4] << 24) | ((unsigned)d[5] << 16) | ((unsigned)d[6] << 8) | d[7];
|
||||
c.depth = d[8];
|
||||
c.color = d[9];
|
||||
if (d[10] != 0 || d[11] != 0 || d[12] != 0) return 0; /* 只支持非交错 + 标准压缩/滤波 */
|
||||
if (c.w == 0 || c.h == 0 || c.w > 65535u || c.h > 65535u) return 0;
|
||||
if (!png_depth_ok(c.color, c.depth)) return 0;
|
||||
c.bits = png_bits_of(c.color, c.depth);
|
||||
c.rowbytes = ((size_t)c.w * c.bits + 7) / 8;
|
||||
if (c.rowbytes == 0) return 0;
|
||||
/* 小图不动:与 meow_img_load_file_dim 的「旧入口」判定一致 */
|
||||
if ((long)(c.w > c.h ? c.w : c.h) <= maxdim) return 0;
|
||||
if (clen > 13 && !ps_skip(&c.src, clen - 13)) return 0;
|
||||
have_ihdr = 1;
|
||||
c.need = c.rowbytes + 1;
|
||||
c.line = (unsigned char *)malloc(c.need);
|
||||
c.prev = (unsigned char *)malloc(c.rowbytes);
|
||||
c.cur = (unsigned char *)malloc(c.rowbytes);
|
||||
if (!c.line || !c.prev || !c.cur) goto done;
|
||||
memset(c.prev, 0, c.rowbytes);
|
||||
memset(c.cur, 0, c.rowbytes);
|
||||
for (i = 0; i < 256; i++) c.trns_a[i] = 255;
|
||||
meow_img_target_dims((int)c.w, (int)c.h, maxdim, &c.tw, &c.th);
|
||||
c.out = (unsigned char *)malloc((size_t)c.tw * c.th * 4);
|
||||
if (!c.out) goto done;
|
||||
memset(c.out, 0, (size_t)c.tw * c.th * 4);
|
||||
memset(&c.z, 0, sizeof c.z);
|
||||
if (inflateInit_(&c.z, zlibVersion(), (int)sizeof c.z) != Z_OK) goto done;
|
||||
c.z_ready = 1;
|
||||
} else if (memcmp(type, "PLTE", 4) == 0) {
|
||||
unsigned char d[256 * 3];
|
||||
if (clen > sizeof d || clen % 3) return 0;
|
||||
if (!ps_read(&c.src, d, clen)) return 0;
|
||||
c.pal_n = (int)(clen / 3);
|
||||
memcpy(c.pal, d, clen);
|
||||
} else if (memcmp(type, "tRNS", 4) == 0) {
|
||||
unsigned char d[256];
|
||||
if (clen > sizeof d) return 0;
|
||||
if (!ps_read(&c.src, d, clen)) return 0;
|
||||
c.have_trns = 1;
|
||||
if (c.color == 3) {
|
||||
for (i = 0; i < (int)clen && i < 256; i++) c.trns_a[i] = d[i];
|
||||
} else if (c.color == 0 && clen >= 2) {
|
||||
/* 8bit/低位深:采样值存在低字节;16bit:与像素一样取高字节比较 */
|
||||
c.trns[0] = (c.depth == 16) ? d[0] : d[1];
|
||||
} else if (c.color == 2 && clen >= 6) {
|
||||
if (c.depth == 16) {
|
||||
c.trns[0] = d[0]; c.trns[1] = d[2]; c.trns[2] = d[4];
|
||||
} else {
|
||||
c.trns[0] = d[1]; c.trns[1] = d[3]; c.trns[2] = d[5];
|
||||
}
|
||||
}
|
||||
} else if (memcmp(type, "IDAT", 4) == 0) {
|
||||
unsigned remain = clen;
|
||||
if (!have_ihdr || !c.out) return 0;
|
||||
while (remain) {
|
||||
unsigned n = remain > sizeof ibuf ? (unsigned)sizeof ibuf : remain;
|
||||
if (!ps_read(&c.src, ibuf, n)) goto done;
|
||||
remain -= n;
|
||||
c.z.next_in = ibuf;
|
||||
c.z.avail_in = n;
|
||||
while (c.z.avail_in && c.next_ty < c.th) {
|
||||
int r = png_pump(&c);
|
||||
if (r == 0) goto done;
|
||||
if (r == PNG_NEED_MORE) break;
|
||||
}
|
||||
}
|
||||
} else if (memcmp(type, "IEND", 4) == 0) {
|
||||
break;
|
||||
} else {
|
||||
if (clen && !ps_skip(&c.src, clen)) return 0;
|
||||
}
|
||||
/* 跳过 CRC */
|
||||
{
|
||||
unsigned char crc[4];
|
||||
if (!ps_read(&c.src, crc, 4)) break;
|
||||
}
|
||||
if (c.out && c.next_ty >= c.th) break; /* 目标行都拿到了,不必读完 */
|
||||
}
|
||||
|
||||
if (!have_ihdr || !c.out || c.next_ty < c.th) goto done;
|
||||
if (c.color == 3 && c.pal_n <= 0) goto done;
|
||||
*tw = c.tw;
|
||||
*th = c.th;
|
||||
*rgba = c.out;
|
||||
c.out = NULL;
|
||||
ok = 1;
|
||||
done:
|
||||
if (c.z_ready) inflateEnd(&c.z);
|
||||
free(c.line);
|
||||
free(c.prev);
|
||||
free(c.cur);
|
||||
free(c.out);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
/* BMP */
|
||||
/* ================================================================== */
|
||||
static unsigned le16(const unsigned char *p) { return (unsigned)p[0] | ((unsigned)p[1] << 8); }
|
||||
static unsigned le32(const unsigned char *p)
|
||||
{
|
||||
return (unsigned)p[0] | ((unsigned)p[1] << 8) | ((unsigned)p[2] << 16) | ((unsigned)p[3] << 24);
|
||||
}
|
||||
|
||||
int meow_bmp_decode_stream(FILE *f, const unsigned char *mem, size_t len,
|
||||
int maxdim, int *tw, int *th, unsigned char **rgba)
|
||||
{
|
||||
png_src src;
|
||||
unsigned char fh[14], dh[4];
|
||||
unsigned char dib[1024];
|
||||
unsigned dibsize, dataoff, bpp, compression, colors;
|
||||
long w, h, rowsize;
|
||||
int topdown, i;
|
||||
unsigned char pal[256][3];
|
||||
unsigned char *row = NULL, *out = NULL;
|
||||
int tw_ = 0, th_ = 0, ok = 0;
|
||||
|
||||
if (maxdim <= 0) return 0;
|
||||
memset(&src, 0, sizeof src);
|
||||
src.f = f;
|
||||
src.mem = mem;
|
||||
src.memlen = len;
|
||||
if (!ps_read(&src, fh, 14)) return 0;
|
||||
if (fh[0] != 'B' || fh[1] != 'M') return 0;
|
||||
dataoff = le32(fh + 10);
|
||||
if (!ps_read(&src, dh, 4)) return 0;
|
||||
dibsize = le32(dh);
|
||||
if (dibsize < 40 || dibsize > sizeof dib) return 0; /* BITMAPCOREHEADER/OS2 → stb */
|
||||
memcpy(dib, dh, 4);
|
||||
if (!ps_read(&src, dib + 4, dibsize - 4)) return 0;
|
||||
w = (long)(int)le32(dib + 4);
|
||||
h = (long)(int)le32(dib + 8);
|
||||
bpp = le16(dib + 14);
|
||||
compression = le32(dib + 16);
|
||||
colors = (dibsize >= 36) ? le32(dib + 32) : 0;
|
||||
if (compression != 0) return 0; /* RLE/BITFIELDS → stb */
|
||||
if (w <= 0 || h == 0) return 0;
|
||||
if (bpp != 8 && bpp != 16 && bpp != 24 && bpp != 32) return 0;
|
||||
topdown = h < 0;
|
||||
if (h < 0) h = -h;
|
||||
if ((long)(w > h ? w : h) <= maxdim) return 0; /* 小图走旧入口 */
|
||||
|
||||
if (bpp <= 8) {
|
||||
unsigned n = colors ? colors : 256;
|
||||
unsigned char e[4];
|
||||
if (n > 256) n = 256;
|
||||
for (i = 0; (unsigned)i < n; i++) {
|
||||
if (!ps_read(&src, e, 4)) return 0;
|
||||
pal[i][0] = e[2]; pal[i][1] = e[1]; pal[i][2] = e[0];
|
||||
}
|
||||
for (; i < 256; i++) pal[i][0] = pal[i][1] = pal[i][2] = 0;
|
||||
}
|
||||
|
||||
rowsize = (w * bpp + 31) / 32 * 4;
|
||||
meow_img_target_dims((int)w, (int)h, maxdim, &tw_, &th_);
|
||||
row = (unsigned char *)malloc((size_t)rowsize);
|
||||
out = (unsigned char *)malloc((size_t)tw_ * th_ * 4);
|
||||
if (!row || !out) goto done;
|
||||
|
||||
for (int ty = 0; ty < th_; ty++) {
|
||||
int sy = (int)((long)ty * h / th_);
|
||||
long file_row = topdown ? sy : (h - 1 - sy);
|
||||
long off = (long)dataoff + file_row * rowsize;
|
||||
unsigned char *o = out + (size_t)ty * tw_ * 4;
|
||||
if (src.mem) {
|
||||
if (off < 0 || (size_t)off + (size_t)rowsize > src.memlen) goto done;
|
||||
memcpy(row, src.mem + off, (size_t)rowsize);
|
||||
} else {
|
||||
if (fseek(src.f, off, SEEK_SET) != 0) goto done;
|
||||
if (fread(row, 1, (size_t)rowsize, src.f) != (size_t)rowsize) goto done;
|
||||
}
|
||||
for (int tx = 0; tx < tw_; tx++) {
|
||||
int sx = (int)((long)tx * w / tw_);
|
||||
unsigned char *p = o + (size_t)tx * 4;
|
||||
if (bpp == 24) {
|
||||
const unsigned char *q = row + sx * 3;
|
||||
p[0] = q[2]; p[1] = q[1]; p[2] = q[0]; p[3] = 255;
|
||||
} else if (bpp == 32) {
|
||||
const unsigned char *q = row + sx * 4;
|
||||
p[0] = q[2]; p[1] = q[1]; p[2] = q[0]; p[3] = 255; /* BI_RGB:alpha 不作为语义 */
|
||||
} else if (bpp == 16) {
|
||||
unsigned v = le16(row + sx * 2);
|
||||
unsigned r = (v >> 10) & 31, g = (v >> 5) & 31, b = v & 31;
|
||||
p[0] = (unsigned char)((r << 3) | (r >> 2));
|
||||
p[1] = (unsigned char)((g << 3) | (g >> 2));
|
||||
p[2] = (unsigned char)((b << 3) | (b >> 2));
|
||||
p[3] = 255;
|
||||
} else {
|
||||
unsigned idx = row[sx];
|
||||
p[0] = pal[idx & 255][0]; p[1] = pal[idx & 255][1];
|
||||
p[2] = pal[idx & 255][2]; p[3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
*tw = tw_;
|
||||
*th = th_;
|
||||
*rgba = out;
|
||||
out = NULL;
|
||||
ok = 1;
|
||||
done:
|
||||
free(row);
|
||||
free(out);
|
||||
return ok;
|
||||
}
|
||||
+109
-16
@@ -20,7 +20,6 @@
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
@@ -172,15 +171,9 @@ static void refresh_governor(unsigned long long cost_ms, int period)
|
||||
}
|
||||
|
||||
/* ---- 工具 ---- */
|
||||
static uint64_t now_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
|
||||
}
|
||||
|
||||
uint64_t meow_ui_time_ms(void) { return now_ms(); }
|
||||
|
||||
/* 时钟统一由 meow_ui_time.c 提供(meow_ui_time_ms,声明见 include/meow_ui_draw.h):
|
||||
* mipsel 上 zig musl 的 time64 ABI 自相矛盾,取时必须走内核 o32 ABI 再升位,
|
||||
* 故本 TU 不再保留取时实现(详见 sdk/CLOCK_ABI.md)。 */
|
||||
static uint16_t rgb565(uint32_t c)
|
||||
{
|
||||
return (uint16_t)(((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x001F));
|
||||
@@ -863,6 +856,106 @@ void meow_gfx_blit_rgba(int x, int y, int w, int h, const uint8_t *rgba, int alp
|
||||
dirty_add(x1, y1, x2 - x1, y2 - y1);
|
||||
}
|
||||
|
||||
/* ================= RGB565 页图(M32) =================
|
||||
* 阅读器页槽常驻 RGB565(2B/px)而不是 RGBA:真机 480×853 页 1599KB → 800KB。
|
||||
* 量化与 back_buf 用同一个 rgb565()(逐位截断),所以 565 屏上落下的像素与旧的
|
||||
* RGBA blit 路径**逐像素一致**——不引入色偏,也不会比现在多出色带。
|
||||
* 命中页(解码/落框尺寸 == 显示尺寸)走 meow_gfx_blit_rgb565:每行一次 memcpy,
|
||||
* 单次拷贝、没有每帧重采样。alpha 按不透明处理(页图 alpha 恒为 255;带透明像素的
|
||||
* PNG/32bpp BMP 页会与旧 RGBA 路径不同——旧路径混合到底色,这里直接画 RGB)。 */
|
||||
uint16_t meow_gfx_rgb565(uint32_t rgb)
|
||||
{
|
||||
return rgb565(rgb);
|
||||
}
|
||||
|
||||
void meow_gfx_rgba_to_rgb565(const uint8_t *rgba, uint16_t *out, int npx)
|
||||
{
|
||||
if (!rgba || !out || npx <= 0) return;
|
||||
for (int i = 0; i < npx; i++) {
|
||||
const uint8_t *p = rgba + (size_t)i * 4;
|
||||
out[i] = rgb565(((uint32_t)p[0] << 16) | ((uint32_t)p[1] << 8) | (uint32_t)p[2]);
|
||||
}
|
||||
}
|
||||
|
||||
/* 目标矩形与 clip 求交;返回 1=还有可画区域。 */
|
||||
static int blit565_clip(int x, int y, int w, int h, int *x1, int *y1, int *x2, int *y2)
|
||||
{
|
||||
if (!g_init_ok || w <= 0 || h <= 0) return 0;
|
||||
*x1 = x; *y1 = y; *x2 = x + w; *y2 = y + h;
|
||||
if (*x1 < 0) *x1 = 0;
|
||||
if (*y1 < 0) *y1 = 0;
|
||||
if (*x2 > g_xres) *x2 = g_xres;
|
||||
if (*y2 > g_yres) *y2 = g_yres;
|
||||
if (g_clip_n > 0) {
|
||||
int *c = g_clip_stack[g_clip_n - 1];
|
||||
if (*x1 < c[0]) *x1 = c[0];
|
||||
if (*y1 < c[1]) *y1 = c[1];
|
||||
if (*x2 > c[2]) *x2 = c[2];
|
||||
if (*y2 > c[3]) *y2 = c[3];
|
||||
}
|
||||
return *x2 > *x1 && *y2 > *y1;
|
||||
}
|
||||
|
||||
void meow_gfx_blit_rgb565(int x, int y, int w, int h, const uint16_t *px)
|
||||
{
|
||||
if (!px) return;
|
||||
int x1, y1, x2, y2;
|
||||
if (!blit565_clip(x, y, w, h, &x1, &y1, &x2, &y2)) return;
|
||||
int n = x2 - x1;
|
||||
for (int py = y1; py < y2; py++) {
|
||||
const uint16_t *src = px + (size_t)(py - y) * w + (x1 - x);
|
||||
unsigned char *dst = g_fb + (size_t)py * g_line + (size_t)x1 * (size_t)(g_bpp / 8);
|
||||
if (g_bpp == 16) {
|
||||
memcpy(dst, src, (size_t)n * 2); /* 命中页:每行一次拷贝 */
|
||||
} else {
|
||||
uint32_t *d32 = (uint32_t *)dst;
|
||||
for (int i = 0; i < n; i++) {
|
||||
uint16_t c = src[i];
|
||||
d32[i] = ((uint32_t)ex5((c >> 11) & 0x1F) << 24) |
|
||||
((uint32_t)ex6((c >> 5) & 0x3F) << 16) |
|
||||
((uint32_t)ex5(c & 0x1F) << 8) | 0xFFu;
|
||||
}
|
||||
}
|
||||
}
|
||||
dirty_add(x1, y1, n, y2 - y1);
|
||||
}
|
||||
|
||||
/* 最近邻缩放(仅几何不符时的兜底路径;采样点与 meow_img_resample 同式)。
|
||||
* 命中页不会走到这里——那是热路径纪律(M17/M32)。 */
|
||||
void meow_gfx_blit_rgb565_scaled(int x, int y, int w, int h,
|
||||
int sw, int sh, const uint16_t *px)
|
||||
{
|
||||
if (!px || sw <= 0 || sh <= 0) return;
|
||||
int x1, y1, x2, y2;
|
||||
if (!blit565_clip(x, y, w, h, &x1, &y1, &x2, &y2)) return;
|
||||
int n = x2 - x1;
|
||||
for (int py = y1; py < y2; py++) {
|
||||
int sy = (int)((long long)(py - y) * sh / h);
|
||||
if (sy >= sh) sy = sh - 1;
|
||||
const uint16_t *row = px + (size_t)sy * sw;
|
||||
unsigned char *dst = g_fb + (size_t)py * g_line + (size_t)x1 * (size_t)(g_bpp / 8);
|
||||
if (g_bpp == 16) {
|
||||
uint16_t *d16 = (uint16_t *)dst;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int sx = (int)((long long)(x1 + i - x) * sw / w);
|
||||
if (sx >= sw) sx = sw - 1;
|
||||
d16[i] = row[sx];
|
||||
}
|
||||
} else {
|
||||
uint32_t *d32 = (uint32_t *)dst;
|
||||
for (int i = 0; i < n; i++) {
|
||||
int sx = (int)((long long)(x1 + i - x) * sw / w);
|
||||
if (sx >= sw) sx = sw - 1;
|
||||
uint16_t c = row[sx];
|
||||
d32[i] = ((uint32_t)ex5((c >> 11) & 0x1F) << 24) |
|
||||
((uint32_t)ex6((c >> 5) & 0x3F) << 16) |
|
||||
((uint32_t)ex5(c & 0x1F) << 8) | 0xFFu;
|
||||
}
|
||||
}
|
||||
}
|
||||
dirty_add(x1, y1, n, y2 - y1);
|
||||
}
|
||||
|
||||
/* ================= present / 上屏 ================= */
|
||||
static void dump_ppm(void)
|
||||
{
|
||||
@@ -895,7 +988,7 @@ static void statusbar_collapse_step(void)
|
||||
int tgt = g_sbar_collapsed ? MUI_SBAR_H : 0;
|
||||
if (g_sbar_collapse_h == tgt) return;
|
||||
if (g_sbar_collapse_anim_ms <= 0) { g_sbar_collapse_h = tgt; return; }
|
||||
unsigned long long now = now_ms();
|
||||
unsigned long long now = meow_ui_time_ms();
|
||||
unsigned long long el = now - g_sbar_collapse_start_ms;
|
||||
int span = g_sbar_collapse_anim_ms;
|
||||
if ((int)el >= span) { g_sbar_collapse_h = tgt; return; }
|
||||
@@ -932,7 +1025,7 @@ void meow_ui_statusbar_set_collapsed(int on)
|
||||
if (tgt == g_sbar_collapsed) return;
|
||||
g_sbar_collapsed = tgt;
|
||||
g_sbar_collapse_from_h = g_sbar_collapse_h;
|
||||
g_sbar_collapse_start_ms = now_ms();
|
||||
g_sbar_collapse_start_ms = meow_ui_time_ms();
|
||||
dirty_full(); /* 顶栏显隐/上收变化 → 全帧重画 */
|
||||
meow_ui_sbar_write_mode(); /* App 进程上报顶条模式(launcher 感知全屏/收合,agent-146) */
|
||||
}
|
||||
@@ -1012,7 +1105,7 @@ int meow_ui_overlay_suppressed_active(void)
|
||||
snprintf(s_path, sizeof s_path, "%s", (e && e[0]) ? e : MEOW_NOTIF_ACTIVE_DEFAULT);
|
||||
s_path_init = 1;
|
||||
}
|
||||
unsigned long long now = now_ms();
|
||||
unsigned long long now = meow_ui_time_ms();
|
||||
if (s_last_chk && now - s_last_chk < MEOW_OVERLAY_CHK_MS) return s_last_res;
|
||||
s_last_chk = now;
|
||||
{
|
||||
@@ -1066,7 +1159,7 @@ static void present(void)
|
||||
}
|
||||
/* 帧率门控(A-1/A-2,从 launcher 迁入):present 距上次 ≥ period(1000/eff_rate) 才呈现;
|
||||
* 未满周期则保留脏区(不 dirty_reset),下一轮再试。refresh_always_max=1 时跳过门控(恒满速)。 */
|
||||
unsigned long long now = now_ms();
|
||||
unsigned long long now = meow_ui_time_ms();
|
||||
int period = (g_eff_rate > 0) ? (1000 / g_eff_rate) : 16;
|
||||
if (!g_refresh_always_max) {
|
||||
if (g_last_present_ms && now - g_last_present_ms < (unsigned long long)period) return;
|
||||
@@ -1096,7 +1189,7 @@ static void present(void)
|
||||
}
|
||||
if (g_fb_mem) {
|
||||
ioctl(g_fb_fd, FBIO_WAITFORVSYNC, 0); /* 等垂直同步 */
|
||||
unsigned long long tbg = now_ms();
|
||||
unsigned long long tbg = meow_ui_time_ms();
|
||||
/* 「同一个状态栏」闭环:App 进程不渲染状态栏(meow_ui_sys_render_enabled()==0),也绝不向
|
||||
* 真实 fb 写顶条 inset 区(顶条由 launcher 系统进程叠加绘制)。否则 App 首帧全量拷贝
|
||||
* (meow_ui_init 置 dirty_full)会把 launcher 刚叠画的顶条冲成未初始化数据——真机
|
||||
@@ -1133,7 +1226,7 @@ static void present(void)
|
||||
}
|
||||
}
|
||||
}
|
||||
refresh_governor(now_ms() - tbg, period);
|
||||
refresh_governor(meow_ui_time_ms() - tbg, period);
|
||||
}
|
||||
g_last_present_ms = now;
|
||||
g_frames_presented++;
|
||||
|
||||
+144
-2
@@ -1,10 +1,11 @@
|
||||
/*
|
||||
* meow_ui_img.c — 图片层(B1.2)。基于 stb_image(单头文件、静态编入),
|
||||
* 解 PNG/JPEG/BMP → RGBA,统一最近邻缩放/居中裁方。
|
||||
* meow_ui_img.c — 图片层(B1.2)。解码:M23 真缩解后端(meow_jpeg.c / meow_png_stream.c)
|
||||
* 优先,不支持/失败回退 stb_image(单头文件、静态编入);统一输出 RGBA,最近邻缩放/居中裁方。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
#include "stb_image.h"
|
||||
#include "meow_jpeg.h" /* M23 真缩解后端(内部头,不对外导出) */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -42,6 +43,147 @@ meow_img *meow_img_load_file(const char *path)
|
||||
return img_alloc(w, h, p);
|
||||
}
|
||||
|
||||
/* 追加(M12):预算解码。选整数缩解档位 s ∈ {1,2,4,8}——取满足「长边/s ≥ maxdim」的最大档,
|
||||
* 保证缩解结果不欠采样(不丢细节),再由 meow_img_downscale 精修到 maxdim。长边 < 2*maxdim
|
||||
* 时无档可用(s=1),退回整幅解码后精修——此时本就接近目标尺寸,无需牺牲。 */
|
||||
int meow_img_pick_scale(int longside, int maxdim)
|
||||
{
|
||||
int s = 1;
|
||||
while (s < 8 && (longside / (s * 2)) >= maxdim) s *= 2;
|
||||
return s;
|
||||
}
|
||||
|
||||
/* 追加(M12):缩解结果落地 + 需要时精修。p 所有权转交。 */
|
||||
static meow_img *img_finish_dim(unsigned char *p, int w, int h, int maxdim)
|
||||
{
|
||||
if (!p) return NULL;
|
||||
meow_img *im = img_alloc(w, h, p);
|
||||
if (!im) return NULL;
|
||||
if (im->w <= maxdim && im->h <= maxdim) return im;
|
||||
meow_img *small = meow_img_downscale(im, maxdim);
|
||||
if (!small) return im; /* 精修失败:缩解结果已小于原图,仍可用 */
|
||||
meow_img_free(im);
|
||||
return small;
|
||||
}
|
||||
|
||||
int meow_img_probe_file(const char *path, int *w, int *h, int *channels)
|
||||
{
|
||||
int pw = 0, ph = 0, pc = 0;
|
||||
if (!path || !stbi_info(path, &pw, &ph, &pc)) return 0;
|
||||
if (w) *w = pw;
|
||||
if (h) *h = ph;
|
||||
if (channels) *channels = pc;
|
||||
return 1;
|
||||
}
|
||||
|
||||
meow_img *meow_img_load_file_dim(const char *path, int maxdim)
|
||||
{
|
||||
if (!path) return NULL;
|
||||
if (maxdim <= 0) return meow_img_load_file(path); /* 不限尺寸 = 旧入口 */
|
||||
/* 追加(M23):真缩解后端直接产出目标尺寸(JPEG 按 DCT 档位;PNG 逐行 inflate 丢行丢列;
|
||||
* BMP 按行 seek 跳采样)。峰值只与输出尺寸相关。
|
||||
* 不支持/失败/几乎等尺寸(4 分量 CMYK、算术编码、交错 PNG、RLE BMP、小图…)返回 0 →
|
||||
* 落回下面的 stb 旧路径,对外语义与档位表现不变。 */
|
||||
{
|
||||
unsigned char *bp = NULL;
|
||||
int bw = 0, bh = 0;
|
||||
if (meow_img_backend_file(path, maxdim, &bw, &bh, &bp) && bp)
|
||||
return img_finish_dim(bp, bw, bh, maxdim);
|
||||
}
|
||||
int w = 0, h = 0, c = 0;
|
||||
if (!stbi_info(path, &w, &h, &c)) return NULL;
|
||||
int longside = w > h ? w : h;
|
||||
if (longside <= maxdim) return meow_img_load_file(path); /* 小图不动:旧路径 */
|
||||
unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, meow_img_pick_scale(longside, maxdim));
|
||||
return img_finish_dim(p, w, h, maxdim);
|
||||
}
|
||||
|
||||
/* 追加(M22):档位由调用方指定。与 meow_img_load_file_dim 同语义(结果长边 ≤ maxdim,
|
||||
* 超了再精修一次),只是档位不再由 maxdim 反推 —— 内存预算紧时可选更粗的档位。
|
||||
* M27:真缩解后端优先(step 折算成目标长边 ceil(长边/step),后端按该长边选 DCT 阶,
|
||||
* 峰值只与输出尺寸相关);后端不支持时回退 stbi_load_scaled(step),语义与 M22 一致。
|
||||
*
|
||||
* M27 r1(P1-1):allow_stb_fallback=0 时只尝试后端,**绝不回退 stb**——阅读器的门控按
|
||||
* 「后端峰值」放行时必须走这里,否则「头部说支持、解码中途失败」会静默落到 stb,
|
||||
* 实测可放出 18–36MB(截断 12MP JPEG / 头-only BMP / 非法位深 PNG),即 M22 的 SIGKILL 场景。
|
||||
* 后端失败返回 NULL(调用方按坏图占位)。 */
|
||||
meow_img *meow_img_load_file_step_ex(const char *path, int maxdim, int step,
|
||||
int allow_stb_fallback, int *used_backend)
|
||||
{
|
||||
if (used_backend) *used_backend = 0;
|
||||
if (!path) return NULL;
|
||||
if (step <= 1 && maxdim <= 0)
|
||||
return allow_stb_fallback ? meow_img_load_file(path) : NULL;
|
||||
if (step <= 0) step = 1;
|
||||
if (step > 8) step = 8;
|
||||
int w = 0, h = 0, c = 0;
|
||||
if (!stbi_info(path, &w, &h, &c)) return NULL;
|
||||
int longside = w > h ? w : h;
|
||||
if (step == 1 && maxdim <= 0)
|
||||
return allow_stb_fallback ? meow_img_load_file(path) : NULL;
|
||||
/* 追加(M23/M27):真缩解后端优先(JPEG/PNG/BMP 全部格式)。step 是「输出别比这个档位
|
||||
* 更细」的提示:折算成目标长边 ceil(长边/step) 传给后端(后端按该长边自动选 DCT 阶),
|
||||
* 与下面 stbi_load_scaled(step) 的语义一致,峰值只与输出尺寸相关;maxdim<=0 时不精修,
|
||||
* 结果仍是档位尺寸。后端不支持(CMYK/算术编码/交错/RLE…)时回退 stb,行为与 M22 一致。
|
||||
* M27 起阅读器的峰值模型已与后端同口径(page_source 的 reader_estimate_peak),
|
||||
* 所以 PNG/BMP 也一并接后端——这正是「修好了却用不上」的最后一处缝。 */
|
||||
{
|
||||
int eff = maxdim > 0 ? maxdim : longside;
|
||||
int step_cap = (longside + step - 1) / step;
|
||||
unsigned char *bp = NULL;
|
||||
int bw = 0, bh = 0;
|
||||
if (step_cap > 0 && step_cap < eff) eff = step_cap;
|
||||
if (eff > 0 && meow_img_backend_file(path, eff, &bw, &bh, &bp) && bp) {
|
||||
if (used_backend) *used_backend = 1;
|
||||
return img_finish_dim(bp, bw, bh, maxdim > 0 ? maxdim : longside);
|
||||
}
|
||||
}
|
||||
if (!allow_stb_fallback) return NULL; /* P1-1:门控按后端放行的页,失败即坏图 */
|
||||
unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, step);
|
||||
/* maxdim<=0:不精修(保留档位输出的原始尺寸)。 */
|
||||
return img_finish_dim(p, w, h, maxdim > 0 ? maxdim : longside);
|
||||
}
|
||||
|
||||
meow_img *meow_img_load_file_step(const char *path, int maxdim, int step)
|
||||
{
|
||||
/* 通用入口保持 M22 语义:后端失败允许回退 stb。 */
|
||||
return meow_img_load_file_step_ex(path, maxdim, step, 1, NULL);
|
||||
}
|
||||
|
||||
/* 追加(M22):精确重采样到目标尺寸(最近邻,双向)。阅读器把缩解档位的结果一次性
|
||||
* 落到显示框,之后命中页就是单次原生 blit(M17 的热路径纪律不被粗档位破坏)。 */
|
||||
static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover);
|
||||
|
||||
meow_img *meow_img_resample(meow_img *im, int w, int h)
|
||||
{
|
||||
if (!im || w <= 0 || h <= 0) return NULL;
|
||||
if (im->w == w && im->h == h) return NULL;
|
||||
unsigned char *tmp = scale_buf(im, w, h, 0);
|
||||
if (!tmp) return NULL;
|
||||
return img_alloc(w, h, tmp);
|
||||
}
|
||||
|
||||
meow_img *meow_img_load_mem_dim(const void *buf, int len, int maxdim)
|
||||
{
|
||||
if (!buf || len <= 0) return NULL;
|
||||
if (maxdim <= 0) return meow_img_load_mem(buf, len);
|
||||
/* 追加(M23):同文件入口,后端直接从调用方缓冲解码(不再整幅解 + 重采样)。 */
|
||||
{
|
||||
unsigned char *bp = NULL;
|
||||
int bw = 0, bh = 0;
|
||||
if (meow_img_backend_mem(buf, (size_t)len, maxdim, &bw, &bh, &bp) && bp)
|
||||
return img_finish_dim(bp, bw, bh, maxdim);
|
||||
}
|
||||
int w = 0, h = 0, c = 0;
|
||||
if (!stbi_info_from_memory((const unsigned char *)buf, len, &w, &h, &c)) return NULL;
|
||||
int longside = w > h ? w : h;
|
||||
if (longside <= maxdim) return meow_img_load_mem(buf, len);
|
||||
unsigned char *p = stbi_load_scaled_from_memory((const unsigned char *)buf, len,
|
||||
&w, &h, &c, 4,
|
||||
meow_img_pick_scale(longside, maxdim));
|
||||
return img_finish_dim(p, w, h, maxdim);
|
||||
}
|
||||
|
||||
void meow_img_free(meow_img *im)
|
||||
{
|
||||
if (!im) return;
|
||||
|
||||
@@ -284,4 +284,8 @@ int meow_ime_handle_event(const meow_input_ev *e);
|
||||
/* 当前 IME 是否拼音模式(供 meow_ui_input.c 决定是否喂长按轮询)。 */
|
||||
int meow_ime_mode_pinyin(void);
|
||||
|
||||
/* ---- 时钟(meow_ui_time.c)---- */
|
||||
/* 秒/纳秒 → 单调毫秒的唯一换算点(meow_ui_time_ms 与宿主单测共用)。 */
|
||||
uint64_t meow_time_ms_from_parts(int64_t sec, int64_t nsec);
|
||||
|
||||
#endif /* MEOW_UI_INTERNAL_H */
|
||||
+20
-5
@@ -52,11 +52,18 @@ struct meow_scroll {
|
||||
int bounce; /* fling 边界回弹幅度(带符号;0=不回弹) */
|
||||
int bounce_start; /* 回弹回位起点(= clamp 边界) */
|
||||
int snap_to; /* snap 目标 */
|
||||
int wrap_slot; /* >=0:占用的 g_wrap_ctx 槽下标(-1=非 wrap 实例) */
|
||||
};
|
||||
|
||||
static meow_scroll g_pool[MEOW_SCROLL_POOL];
|
||||
static unsigned char g_used[MEOW_SCROLL_POOL];
|
||||
|
||||
/* wrap 上下文槽:meow_scroll_wrap 占用一个,meow_scroll_free 必须归还(否则进程内最多 wrap
|
||||
* MEOW_SCROLL_POOL 次后恒 NULL)。槽由 wrap 实例独占,free 时按 wrap_slot 精确归还。 */
|
||||
typedef struct { meow_widget *w; int lv; } wrap_ctx;
|
||||
static wrap_ctx g_wrap_ctx[MEOW_SCROLL_POOL];
|
||||
static int g_wrap_used[MEOW_SCROLL_POOL];
|
||||
|
||||
/* ---- 生命周期 ---- */
|
||||
meow_scroll *meow_scroll_create(meow_scroll_set_fn set, meow_scroll_max_fn max,
|
||||
meow_scroll_get_fn get, meow_scroll_redraw_fn redraw,
|
||||
@@ -70,6 +77,7 @@ meow_scroll *meow_scroll_create(meow_scroll_set_fn set, meow_scroll_max_fn max,
|
||||
memset(s, 0, sizeof *s);
|
||||
s->set = set; s->max = max; s->get = get; s->redraw = redraw; s->ctx = ctx;
|
||||
s->tau_ms = MEOW_SCROLL_TAU;
|
||||
s->wrap_slot = -1;
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -81,11 +89,20 @@ void meow_scroll_free(meow_scroll *s)
|
||||
if (s->anim) meow_anim_cancel(s->anim);
|
||||
s->anim = NULL;
|
||||
for (int i = 0; i < MEOW_SCROLL_POOL; i++)
|
||||
if (&g_pool[i] == s) { g_used[i] = 0; return; }
|
||||
if (&g_pool[i] == s) {
|
||||
int ws = s->wrap_slot;
|
||||
if (ws >= 0 && ws < MEOW_SCROLL_POOL) { /* wrap 实例:归还上下文槽 */
|
||||
g_wrap_ctx[ws].w = NULL;
|
||||
g_wrap_ctx[ws].lv = 0;
|
||||
g_wrap_used[ws] = 0;
|
||||
}
|
||||
s->wrap_slot = -1; /* 二次 free 时不再触碰任何槽 */
|
||||
g_used[i] = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- wrap:包一个 meow_widget 列表 ---- */
|
||||
typedef struct { meow_widget *w; int lv; } wrap_ctx;
|
||||
static void wrap_set(void *c, int off)
|
||||
{ wrap_ctx *wc = c; if (wc->lv) meow_listview_set_scroll(wc->w, off); else meow_list_set_scroll(wc->w, off); }
|
||||
static int wrap_max(void *c)
|
||||
@@ -100,9 +117,6 @@ static void wrap_redraw(void *c)
|
||||
if (r[2] > 0 && r[3] > 0) meow_ui_flush_region(r[0], r[1], r[2], r[3]); /* 只刷视口区 */
|
||||
}
|
||||
|
||||
static wrap_ctx g_wrap_ctx[MEOW_SCROLL_POOL];
|
||||
static int g_wrap_used[MEOW_SCROLL_POOL];
|
||||
|
||||
meow_scroll *meow_scroll_wrap(meow_widget *list, int is_listview)
|
||||
{
|
||||
if (!list) return NULL;
|
||||
@@ -113,6 +127,7 @@ meow_scroll *meow_scroll_wrap(meow_widget *list, int is_listview)
|
||||
g_wrap_used[i] = 1;
|
||||
g_wrap_ctx[i].w = list; g_wrap_ctx[i].lv = is_listview ? 1 : 0;
|
||||
s->ctx = &g_wrap_ctx[i];
|
||||
s->wrap_slot = i;
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/* meow_ui_time.c — SDK 单调时钟(meow_ui_time_ms)+ mipsel o32 时间 ABI 兜底。
|
||||
*
|
||||
* 根因(M11;真机日志 + 反汇编证据链见 sdk/CLOCK_ABI.md):
|
||||
* zig 0.17 的 clang 前端给 32 位 mips 目标预定义了 __ILP32__;zig 自带 musl 的
|
||||
* time64 回退分支统一写作 `#if defined(SYS_x_time64) && !defined(__ILP32__)`
|
||||
* (该 `!__ILP32__` 本意只排除 x32),于是 mipsel 的 libc 把「time64 入口」编译成了
|
||||
* 老 32 位实现:__clock_gettime64 直接做 SYS_clock_gettime(4263),只写 8 字节
|
||||
* `{ int32 tv_sec; int32 tv_nsec; }`。而 zig 头文件又无条件把 time_t 声明成 64 位
|
||||
* (struct timespec 16 字节、tv_sec 8 字节),两边 ABI 不一致:
|
||||
* tv_sec 低字 = 真 sec,高字 = 真 nsec → 64 位结果高 32 位随机、非单调;
|
||||
* tv_nsec(结构偏移 8)根本没被写 → 毫秒位恒 0(真机实测全是 1000 的倍数)。
|
||||
* 容器旧链(gcc)不定义 __ILP32__,time64 回退正确——对照 patch_v2/ncm_app/ncm:
|
||||
* 同一函数里先 SYS_clock_gettime64(4403)、ENOSYS 后走 4263 并做宽度转换。
|
||||
*
|
||||
* 修复:不依赖 libc 头声明的 struct timespec 宽度,直接按**内核 o32 ABI**
|
||||
* (8 字节 time32:int32 sec + int32 nsec)取时,再自行升成 64 位毫秒;
|
||||
* 同时在本 TU 给出 __clock_gettime / __clock_gettime64 的强定义。应用链接顺序是
|
||||
* `... meow_ui.a ... -lc`,归档先参与解析,因此链接期整体替换掉 libc 里那份
|
||||
* ABI 不一致的实现:time()、gettimeofday()、pthread 定时等待的内部取时
|
||||
* (musl 的 time()/__timedwait_cp 都调 __clock_gettime)随之修正。
|
||||
* 上层时间位宽仍是 64 位,只有内核取数按 o32 time32;2038 前有效。
|
||||
*
|
||||
* 同一句 `!__ILP32__` 还打穿了睡眠:__nanosleep_time64/__clock_nanosleep 走 `#else`
|
||||
* 把 16 字节 time64 结构直接交给 time32 系统调用,内核读到的 nsec 恒为 0 →
|
||||
* `usleep(16000)` 立即返回(SDK 里帧节拍/轮询会变忙等空转)。这里一并按 time32
|
||||
* 转换后重发,恢复亚秒睡眠。
|
||||
*
|
||||
* 残余(未修):futex 相对超时(pthread 定时等待的剩余时间按 8 字节读)、timerfd_settime、
|
||||
* clock_settime/adjtime、pselect/ppoll 超时——同为该守卫的受害者,见 CLOCK_ABI.md §6。
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "meow_ui.h"
|
||||
|
||||
#if defined(__mips__) && !defined(__mips64) && (__SIZEOF_LONG__ == 4)
|
||||
#define MEOW_TIME32_KERNEL_ABI 1
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#define MEOW_TIME32_KERNEL_ABI 0
|
||||
#endif
|
||||
|
||||
uint64_t meow_time_ms_from_parts(int64_t sec, int64_t nsec)
|
||||
{
|
||||
return (uint64_t)sec * 1000u + (uint64_t)nsec / 1000000u;
|
||||
}
|
||||
|
||||
uint64_t meow_ui_time_ms(void)
|
||||
{
|
||||
#if MEOW_TIME32_KERNEL_ABI
|
||||
struct meow_time32 { int32_t tv_sec; int32_t tv_nsec; } t;
|
||||
t.tv_sec = 0;
|
||||
t.tv_nsec = 0;
|
||||
if (syscall(SYS_clock_gettime32, CLOCK_MONOTONIC, &t) != 0) return 0;
|
||||
return meow_time_ms_from_parts(t.tv_sec, t.tv_nsec);
|
||||
#else
|
||||
struct timespec ts;
|
||||
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0;
|
||||
return meow_time_ms_from_parts((int64_t)ts.tv_sec, (int64_t)ts.tv_nsec);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MEOW_TIME32_KERNEL_ABI
|
||||
/* 内核 o32 time32 布局(与 libc 头的 time_t 宽度无关)。 */
|
||||
struct meow_time32 {
|
||||
int32_t tv_sec;
|
||||
int32_t tv_nsec;
|
||||
};
|
||||
|
||||
static int32_t time32_clamp(int64_t v)
|
||||
{
|
||||
if (v > INT32_MAX) return INT32_MAX;
|
||||
if (v < 0) return 0;
|
||||
return (int32_t)v;
|
||||
}
|
||||
|
||||
/* ---- libc 取时入口的强定义 ----
|
||||
* 返回约定与 musl 一致:0 成功 / -1 失败(errno)。
|
||||
* 说明:<time.h> 在 _REDIR_TIME64 下把标识符 clock_gettime 的引用改名为
|
||||
* __clock_gettime64,故本 TU 不定义 clock_gettime 这个名字;musl 里它只是
|
||||
* __clock_gettime 的 weak alias,同一份语义已由下面的强定义接管。 */
|
||||
int __clock_gettime(clockid_t clk, struct timespec *ts);
|
||||
int __clock_gettime64(clockid_t clk, struct timespec *ts);
|
||||
|
||||
int __clock_gettime(clockid_t clk, struct timespec *ts)
|
||||
{
|
||||
struct meow_time32 t;
|
||||
if (!ts) { errno = EFAULT; return -1; }
|
||||
t.tv_sec = 0;
|
||||
t.tv_nsec = 0;
|
||||
if (syscall(SYS_clock_gettime32, clk, &t) != 0) return -1;
|
||||
ts->tv_sec = t.tv_sec;
|
||||
ts->tv_nsec = t.tv_nsec;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __clock_gettime64(clockid_t clk, struct timespec *ts)
|
||||
{
|
||||
return __clock_gettime(clk, ts);
|
||||
}
|
||||
|
||||
/* ---- 睡眠入口(同源缺陷:亚秒 usleep 变空转)----
|
||||
* __clock_nanosleep 按 musl 内部约定返回 0 或正 errno;__nanosleep_time64 是
|
||||
* nanosleep/usleep 的重定向目标,按公共约定返回 0/-1(errno)。 */
|
||||
int __clock_nanosleep(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem);
|
||||
int __clock_nanosleep_time64(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem);
|
||||
int __nanosleep_time64(const struct timespec *req, struct timespec *rem);
|
||||
|
||||
int __clock_nanosleep(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem)
|
||||
{
|
||||
struct meow_time32 rq, rm;
|
||||
long r;
|
||||
|
||||
if (clk == CLOCK_THREAD_CPUTIME_ID) return EINVAL;
|
||||
if (!req) return EFAULT;
|
||||
if (req->tv_nsec < 0 || req->tv_nsec >= 1000000000L) return EINVAL;
|
||||
rq.tv_sec = time32_clamp(req->tv_sec);
|
||||
rq.tv_nsec = (int32_t)req->tv_nsec;
|
||||
rm.tv_sec = 0;
|
||||
rm.tv_nsec = 0;
|
||||
|
||||
if (clk == CLOCK_REALTIME && !flags)
|
||||
r = syscall(SYS_nanosleep, &rq, rem ? &rm : 0);
|
||||
else
|
||||
r = syscall(SYS_clock_nanosleep_time32, clk, flags, &rq, rem ? &rm : 0);
|
||||
if (r != 0) return errno;
|
||||
if (rem) {
|
||||
rem->tv_sec = rm.tv_sec;
|
||||
rem->tv_nsec = rm.tv_nsec;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __clock_nanosleep_time64(clockid_t clk, int flags, const struct timespec *req, struct timespec *rem)
|
||||
{
|
||||
return __clock_nanosleep(clk, flags, req, rem);
|
||||
}
|
||||
|
||||
int __nanosleep_time64(const struct timespec *req, struct timespec *rem)
|
||||
{
|
||||
int r = __clock_nanosleep(CLOCK_REALTIME, 0, req, rem);
|
||||
if (r) { errno = r; return -1; }
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
+80
-12
@@ -429,6 +429,14 @@ STBIDEF stbi_uc *stbi_load_from_file (FILE *f, int *x, int *y, int *channels_in
|
||||
// for stbi_load_from_file, file pointer is left pointing immediately after image
|
||||
#endif
|
||||
|
||||
// 追加(MeowOS SDK M12):JPEG 解码期降采样。scale>=2 时按 s 抽取输出行/列,
|
||||
// 不物化整幅原尺寸输出缓冲(*x,*y 返回 ceil(原尺寸/s))。scale<=1 与
|
||||
// stbi_load / stbi_load_from_memory 完全等价;非 JPEG 格式忽略 scale。
|
||||
STBIDEF stbi_uc *stbi_load_scaled_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels, int scale);
|
||||
#ifndef STBI_NO_STDIO
|
||||
STBIDEF stbi_uc *stbi_load_scaled (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels, int scale);
|
||||
#endif
|
||||
|
||||
#ifndef STBI_NO_GIF
|
||||
STBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp);
|
||||
#endif
|
||||
@@ -907,7 +915,7 @@ typedef struct
|
||||
|
||||
#ifndef STBI_NO_JPEG
|
||||
static int stbi__jpeg_test(stbi__context *s);
|
||||
static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri);
|
||||
static void *stbi__jpeg_load_ex(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int scale);
|
||||
static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp);
|
||||
#endif
|
||||
|
||||
@@ -1134,7 +1142,7 @@ STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_fli
|
||||
: stbi__vertically_flip_on_load_global)
|
||||
#endif // STBI_THREAD_LOCAL
|
||||
|
||||
static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc)
|
||||
static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc, int jpeg_scale)
|
||||
{
|
||||
memset(ri, 0, sizeof(*ri)); // make sure it's initialized if we add new fields
|
||||
ri->bits_per_channel = 8; // default is 8 so most paths don't have to be changed
|
||||
@@ -1165,7 +1173,9 @@ static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int re
|
||||
// bytes matching expectations; these are prone to false positives, so
|
||||
// try them later
|
||||
#ifndef STBI_NO_JPEG
|
||||
if (stbi__jpeg_test(s)) return stbi__jpeg_load(s,x,y,comp,req_comp, ri);
|
||||
if (stbi__jpeg_test(s)) return stbi__jpeg_load_ex(s,x,y,comp,req_comp, ri, jpeg_scale);
|
||||
#else
|
||||
STBI_NOTUSED(jpeg_scale);
|
||||
#endif
|
||||
#ifndef STBI_NO_PNM
|
||||
if (stbi__pnm_test(s)) return stbi__pnm_load(s,x,y,comp,req_comp, ri);
|
||||
@@ -1257,10 +1267,10 @@ static void stbi__vertical_flip_slices(void *image, int w, int h, int z, int byt
|
||||
}
|
||||
#endif
|
||||
|
||||
static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp)
|
||||
static unsigned char *stbi__load_and_postprocess_8bit_ex(stbi__context *s, int *x, int *y, int *comp, int req_comp, int jpeg_scale)
|
||||
{
|
||||
stbi__result_info ri;
|
||||
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8);
|
||||
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8, jpeg_scale);
|
||||
|
||||
if (result == NULL)
|
||||
return NULL;
|
||||
@@ -1283,10 +1293,15 @@ static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x,
|
||||
return (unsigned char *) result;
|
||||
}
|
||||
|
||||
static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp)
|
||||
{
|
||||
return stbi__load_and_postprocess_8bit_ex(s, x, y, comp, req_comp, 1);
|
||||
}
|
||||
|
||||
static stbi__uint16 *stbi__load_and_postprocess_16bit(stbi__context *s, int *x, int *y, int *comp, int req_comp)
|
||||
{
|
||||
stbi__result_info ri;
|
||||
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16);
|
||||
void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16, 1);
|
||||
|
||||
if (result == NULL)
|
||||
return NULL;
|
||||
@@ -1409,6 +1424,19 @@ STBIDEF stbi_us *stbi_load_16(char const *filename, int *x, int *y, int *comp, i
|
||||
return result;
|
||||
}
|
||||
|
||||
// 追加(M12):JPEG 解码期降采样(见头文件声明)。非 JPEG 等价于 stbi_load。
|
||||
STBIDEF stbi_uc *stbi_load_scaled(char const *filename, int *x, int *y, int *comp, int req_comp, int scale)
|
||||
{
|
||||
FILE *f = stbi__fopen(filename, "rb");
|
||||
stbi_uc *result;
|
||||
stbi__context s;
|
||||
if (!f) return stbi__errpuc("can't fopen", "Unable to open file");
|
||||
stbi__start_file(&s,f);
|
||||
result = stbi__load_and_postprocess_8bit_ex(&s,x,y,comp,req_comp, scale);
|
||||
if (result) fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR);
|
||||
fclose(f);
|
||||
return result;
|
||||
}
|
||||
|
||||
#endif //!STBI_NO_STDIO
|
||||
|
||||
@@ -1433,6 +1461,14 @@ STBIDEF stbi_uc *stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, i
|
||||
return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp);
|
||||
}
|
||||
|
||||
// 追加(M12):JPEG 解码期降采样(见头文件声明)。非 JPEG 等价于 stbi_load_from_memory。
|
||||
STBIDEF stbi_uc *stbi_load_scaled_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp, int scale)
|
||||
{
|
||||
stbi__context s;
|
||||
stbi__start_mem(&s,buffer,len);
|
||||
return stbi__load_and_postprocess_8bit_ex(&s,x,y,comp,req_comp, scale);
|
||||
}
|
||||
|
||||
STBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp)
|
||||
{
|
||||
stbi__context s;
|
||||
@@ -1982,6 +2018,7 @@ typedef struct
|
||||
int nomore; // flag if we saw a marker so must stop
|
||||
|
||||
int progressive;
|
||||
int scale; // 追加(M12):输出抽取步长,默认 1=原尺寸
|
||||
int spec_start;
|
||||
int spec_end;
|
||||
int succ_high;
|
||||
@@ -3092,6 +3129,12 @@ static void stbi__jpeg_finish(stbi__jpeg *z)
|
||||
z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data);
|
||||
}
|
||||
}
|
||||
// 追加(M12):该分量系数已全部 IDCT 进像素平面,立即释放,降低进升级 JPEG 的峰值
|
||||
if (z->img_comp[n].raw_coeff) {
|
||||
STBI_FREE(z->img_comp[n].raw_coeff);
|
||||
z->img_comp[n].raw_coeff = 0;
|
||||
z->img_comp[n].coeff = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3893,6 +3936,10 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
|
||||
unsigned int i,j;
|
||||
stbi_uc *output;
|
||||
stbi_uc *coutput[4] = { NULL, NULL, NULL, NULL };
|
||||
stbi_uc *rowbuf = NULL; // 追加(M12):整行临时缓冲,仅 dec>1 时使用
|
||||
int dec = z->scale > 1 ? z->scale : 1;
|
||||
int out_w = (z->s->img_x + dec - 1) / dec;
|
||||
int out_h = (z->s->img_y + dec - 1) / dec;
|
||||
|
||||
stbi__resample res_comp[4];
|
||||
|
||||
@@ -3918,13 +3965,19 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
|
||||
else r->resample = stbi__resample_row_generic;
|
||||
}
|
||||
|
||||
// 追加(M12):dec>1 时整行先转成像素再按步长抽取,不物化整幅原尺寸输出缓冲
|
||||
if (dec > 1) {
|
||||
rowbuf = (stbi_uc *) stbi__malloc_mad2(n, z->s->img_x, 0);
|
||||
if (!rowbuf) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); }
|
||||
}
|
||||
|
||||
// can't error after this so, this is safe
|
||||
output = (stbi_uc *) stbi__malloc_mad3(n, z->s->img_x, z->s->img_y, 1);
|
||||
if (!output) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); }
|
||||
output = (stbi_uc *) stbi__malloc_mad3(n, out_w, out_h, 1);
|
||||
if (!output) { STBI_FREE(rowbuf); stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); }
|
||||
|
||||
// now go ahead and resample
|
||||
for (j=0; j < z->s->img_y; ++j) {
|
||||
stbi_uc *out = output + n * z->s->img_x * j;
|
||||
stbi_uc *out;
|
||||
for (k=0; k < decode_n; ++k) {
|
||||
stbi__resample *r = &res_comp[k];
|
||||
int y_bot = r->ystep >= (r->vs >> 1);
|
||||
@@ -3939,6 +3992,9 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
|
||||
r->line1 += z->img_comp[k].w2;
|
||||
}
|
||||
}
|
||||
// 追加(M12):resample 状态机须逐行推进(上面已完成);非采样行直接丢弃
|
||||
if (dec > 1 && (j % (unsigned int) dec) != 0) continue;
|
||||
out = (dec > 1) ? rowbuf : output + (size_t) n * out_w * j;
|
||||
if (n >= 3) {
|
||||
stbi_uc *y = coutput[0];
|
||||
if (z->s->img_n == 3) {
|
||||
@@ -4016,16 +4072,27 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp
|
||||
for (i=0; i < z->s->img_x; ++i) { *out++ = y[i]; *out++ = 255; }
|
||||
}
|
||||
}
|
||||
// 追加(M12):rowbuf 已按整行转好像素,按步长抽取写入输出行
|
||||
if (dec > 1) {
|
||||
stbi_uc *dst = output + (size_t) n * out_w * (j / (unsigned int) dec);
|
||||
unsigned int x;
|
||||
for (x=0; x < (unsigned int) out_w; ++x) {
|
||||
int c;
|
||||
for (c=0; c < n; ++c)
|
||||
dst[(size_t) x * n + c] = rowbuf[(size_t) x * dec * n + c];
|
||||
}
|
||||
}
|
||||
}
|
||||
STBI_FREE(rowbuf);
|
||||
stbi__cleanup_jpeg(z);
|
||||
*out_x = z->s->img_x;
|
||||
*out_y = z->s->img_y;
|
||||
*out_x = out_w;
|
||||
*out_y = out_h;
|
||||
if (comp) *comp = z->s->img_n >= 3 ? 3 : 1; // report original components, not output
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri)
|
||||
static void *stbi__jpeg_load_ex(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int scale)
|
||||
{
|
||||
unsigned char* result;
|
||||
stbi__jpeg* j = (stbi__jpeg*) stbi__malloc(sizeof(stbi__jpeg));
|
||||
@@ -4033,6 +4100,7 @@ static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int re
|
||||
memset(j, 0, sizeof(stbi__jpeg));
|
||||
STBI_NOTUSED(ri);
|
||||
j->s = s;
|
||||
j->scale = scale; // 追加(M12):输出抽取步长
|
||||
stbi__setup_jpeg(j);
|
||||
result = load_jpeg_image(j, x,y,comp,req_comp);
|
||||
STBI_FREE(j);
|
||||
|
||||
+3
-2
@@ -8,8 +8,9 @@
|
||||
* gcc -Wall -Wextra -o test_ime_fix test_ime_fix.c test_host_stub.c \
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_media.c \
|
||||
* meow_ui_img.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* (meow_ui_widget.c 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm)
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
* - MEOW_CACHEGOV_C_FILE 指向临时文件写入 fake C 状态,断言 download_paused/prefetch_paused。
|
||||
*
|
||||
* 编译(patch_v2/sdk 目录下):
|
||||
* gcc -Os -Wall -Wextra -I. -Iinclude -I../meow_launcherd -c meow_cachegov.c -o /tmp/cg.o
|
||||
* gcc -Os -Wall -Wextra -I. -Iinclude -I../meow_launcherd test_meow_cachegov.c /tmp/cg.o -lpthread -o test_meow_cachegov
|
||||
* gcc -Os -Wall -Wextra -I. -Iinclude -c meow_cachegov.c -o /tmp/cg.o
|
||||
* gcc -Os -Wall -Wextra -I. -Iinclude test_meow_cachegov.c /tmp/cg.o -lpthread -o test_meow_cachegov
|
||||
* 运行:./test_meow_cachegov (exit 0=PASS / 1=FAIL)
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
|
||||
@@ -0,0 +1,466 @@
|
||||
/* test_meow_img_budget.c — M12 预算解码(meow_img_load_file_dim / _mem_dim)宿主验证。
|
||||
*
|
||||
* 覆盖矩阵(对齐 M12 任务 3/4):
|
||||
* B1 探针:meow_img_probe_file 只读头取原始尺寸(8.32MP 图),坏图/不存在返回 0
|
||||
* B2 小图不动:64x64 走旧路径,与 meow_img_load_file 逐字节相同;maxdim<=0 等价旧入口
|
||||
* B3 长边约束:8.32MP baseline JPEG 在 maxdim=480/300/128/64 下结果两维都 ≤ maxdim,长宽比保持
|
||||
* B4 峰值对比:同一 8.32MP JPEG「旧入口 vs 新入口」瞬时分配峰值(--wrap malloc 打点),
|
||||
* 断言新入口峰值 < 旧入口 1/2;B4b 极端 maxdim=100;B4c finding 原始场景 1600x2400 页
|
||||
* B5 内容正确:新入口结果与「旧入口整幅解码 + meow_img_downscale」网格采样比对(平均差阈值)
|
||||
* B6 进升级:progressive JPEG 走同一缩解路径(峰值记录 + 长边约束)
|
||||
* B7 内存入口:meow_img_load_mem_dim 与文件入口等价(峰值 < 旧内存入口 1/2)
|
||||
* B8 坏图:坏 JPEG(魔数+垃圾)/ 空内存 / 不存在路径 → NULL(不崩)
|
||||
* B9 PNG/BMP:尺寸约束成立;M23 起 PNG/BMP 也走解码期缩解(逐行 inflate / 按行 seek),
|
||||
* 峰值应显著低于旧入口(本用例只断言 ≤ maxdim + 非空,并打印峰值供报告引用)
|
||||
*
|
||||
* 夹具(生成一次):
|
||||
* python3 test_meow_img_budget_gen.py "$PREFIX/tmp/meow_img_budget"
|
||||
* 编译(本 sdk 目录下;--wrap 用于给 malloc/free 家族打点;-lz 供 PNG 后端 inflate):
|
||||
* gcc -Wall -Wextra -I. -Iinclude -DMEOW_UI_HOST_TEST -o test_meow_img_budget \
|
||||
* test_meow_img_budget.c meow_ui_img.c meow_jpeg.c meow_png_stream.c stb_image.c \
|
||||
* -lm -lz \
|
||||
* -Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc -Wl,--wrap=free
|
||||
* 运行:./test_meow_img_budget "$PREFIX/tmp/meow_img_budget" (exit 0=PASS / 1=FAIL)
|
||||
* 夹具目录可省略:默认 $PREFIX/tmp/meow_img_budget;缺失时自动调 gen 脚本生成。
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h" /* meow_gfx_blit_rgba 原型(本测试只给桩) */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 分配打点:--wrap malloc/free 家族,记录「存活字节」与峰值 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
#define AHDR 32 /* 头部长度:保留原始大小,保证 16 字节对齐 */
|
||||
|
||||
static size_t g_live = 0;
|
||||
static size_t g_peak = 0;
|
||||
|
||||
extern void *__real_malloc(size_t);
|
||||
extern void *__real_calloc(size_t, size_t);
|
||||
extern void *__real_realloc(void *, size_t);
|
||||
extern void __real_free(void *);
|
||||
|
||||
static void acc_alloc(size_t n)
|
||||
{
|
||||
g_live += n;
|
||||
if (g_live > g_peak) g_peak = g_live;
|
||||
}
|
||||
|
||||
void *__wrap_malloc(size_t n)
|
||||
{
|
||||
size_t *h = (size_t *)__real_malloc(n + AHDR);
|
||||
if (!h) return NULL;
|
||||
h[0] = n;
|
||||
acc_alloc(n);
|
||||
return (char *)h + AHDR;
|
||||
}
|
||||
|
||||
void *__wrap_calloc(size_t a, size_t b)
|
||||
{
|
||||
size_t n = a * b;
|
||||
void *p = __wrap_malloc(n);
|
||||
if (p) memset(p, 0, n);
|
||||
return p;
|
||||
}
|
||||
|
||||
void __wrap_free(void *p)
|
||||
{
|
||||
size_t *h;
|
||||
if (!p) return;
|
||||
h = (size_t *)((char *)p - AHDR);
|
||||
g_live -= h[0];
|
||||
__real_free(h);
|
||||
}
|
||||
|
||||
void *__wrap_realloc(void *p, size_t n)
|
||||
{
|
||||
size_t old = p ? ((size_t *)((char *)p - AHDR))[0] : 0;
|
||||
void *q = __wrap_malloc(n);
|
||||
if (!q) return NULL;
|
||||
if (p) {
|
||||
memcpy(q, p, old < n ? old : n);
|
||||
__wrap_free(p);
|
||||
}
|
||||
return q;
|
||||
}
|
||||
|
||||
static size_t peak_reset(void) { g_peak = g_live; return g_live; }
|
||||
static size_t peak_since(size_t base) { return g_peak - base; }
|
||||
|
||||
/* meow_ui_img.c 的绘制函数需要的 gfx 符号:本测试只测解码/内存,桩即可。 */
|
||||
void meow_gfx_blit_rgba(int x, int y, int w, int h, const uint8_t *rgba, int alpha)
|
||||
{
|
||||
(void)x; (void)y; (void)w; (void)h; (void)rgba; (void)alpha;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 断言与打印 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond, name) do { \
|
||||
if (cond) printf(" PASS %s\n", name); \
|
||||
else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fail++; } } while (0)
|
||||
|
||||
static void peak_report(const char *tag, size_t bytes)
|
||||
{
|
||||
printf(" peak %-28s %8.2f MiB (%zu B)\n", tag, (double)bytes / (1024.0 * 1024.0), bytes);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 路径与夹具 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static const char *g_dir = NULL;
|
||||
|
||||
static char *path_join(const char *name)
|
||||
{
|
||||
size_t n = strlen(g_dir) + strlen(name) + 2;
|
||||
char *p = (char *)malloc(n);
|
||||
if (!p) { printf("OOM\n"); exit(1); }
|
||||
snprintf(p, n, "%s/%s", g_dir, name);
|
||||
return p;
|
||||
}
|
||||
|
||||
static int file_exists(const char *p)
|
||||
{
|
||||
struct stat st;
|
||||
return stat(p, &st) == 0 && st.st_size > 0;
|
||||
}
|
||||
|
||||
static void ensure_fixtures(void)
|
||||
{
|
||||
char *big = path_join("big.jpg");
|
||||
if (file_exists(big)) { free(big); return; }
|
||||
free(big);
|
||||
printf("夹具缺失,尝试 python3 test_meow_img_budget_gen.py %s\n", g_dir);
|
||||
{
|
||||
char cmd[1024];
|
||||
snprintf(cmd, sizeof cmd, "python3 test_meow_img_budget_gen.py '%s'", g_dir);
|
||||
if (system(cmd) != 0)
|
||||
printf(" 生成失败(缺 Pillow?)——请按头注释手动生成\n");
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t *read_whole(const char *p, size_t *len)
|
||||
{
|
||||
FILE *f = fopen(p, "rb");
|
||||
uint8_t *buf;
|
||||
long n;
|
||||
if (!f) return NULL;
|
||||
fseek(f, 0, SEEK_END);
|
||||
n = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (n <= 0) { fclose(f); return NULL; }
|
||||
buf = (uint8_t *)malloc((size_t)n);
|
||||
if (!buf) { fclose(f); return NULL; }
|
||||
if (fread(buf, 1, (size_t)n, f) != (size_t)n) { free(buf); fclose(f); return NULL; }
|
||||
fclose(f);
|
||||
*len = (size_t)n;
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 图像辅助 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
static int nonblank(const meow_img *im)
|
||||
{
|
||||
int w = meow_img_w(im), h = meow_img_h(im);
|
||||
const unsigned char *p = meow_img_pixels(im);
|
||||
long sum = 0;
|
||||
if (!p || w <= 0 || h <= 0) return 0;
|
||||
for (long i = 0; i < (long)w * h; i += 97)
|
||||
sum += p[i * 4] + p[i * 4 + 1] + p[i * 4 + 2];
|
||||
return sum > 0;
|
||||
}
|
||||
|
||||
/* 按比例网格采样比对两图(尺寸可不同):返回各通道平均绝对差。 */
|
||||
static double mean_abs_diff(const meow_img *a, const meow_img *b, int grid)
|
||||
{
|
||||
int wa = meow_img_w(a), ha = meow_img_h(a);
|
||||
int wb = meow_img_w(b), hb = meow_img_h(b);
|
||||
const unsigned char *pa = meow_img_pixels(a), *pb = meow_img_pixels(b);
|
||||
long acc = 0, cnt = 0;
|
||||
if (!pa || !pb || wa <= 0 || hb <= 0) return 1e9;
|
||||
for (int gy = 0; gy < grid; gy++) {
|
||||
int ya = (int)((long)gy * ha / grid);
|
||||
int yb = (int)((long)gy * hb / grid);
|
||||
for (int gx = 0; gx < grid; gx++) {
|
||||
int xa = (int)((long)gx * wa / grid);
|
||||
int xb = (int)((long)gx * wb / grid);
|
||||
const unsigned char *qa = pa + ((size_t)ya * wa + xa) * 4;
|
||||
const unsigned char *qb = pb + ((size_t)yb * wb + xb) * 4;
|
||||
for (int k = 0; k < 3; k++) {
|
||||
int d = (int)qa[k] - (int)qb[k];
|
||||
acc += d < 0 ? -d : d;
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cnt ? (double)acc / (double)cnt : 1e9;
|
||||
}
|
||||
|
||||
/* ================================================================== */
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
char *p_big, *p_prog, *p_small, *p_png, *p_bmp, *p_broken, *p_missing;
|
||||
size_t peak_old, peak_new;
|
||||
|
||||
if (argc > 1) {
|
||||
g_dir = argv[1];
|
||||
} else {
|
||||
/* Termux 的 /tmp 不可写:优先 $TMPDIR,其次 $PREFIX/tmp。 */
|
||||
static char dflt[600];
|
||||
const char *tmp = getenv("TMPDIR");
|
||||
const char *pre = getenv("PREFIX");
|
||||
if (tmp && *tmp) snprintf(dflt, sizeof dflt, "%s/meow_img_budget", tmp);
|
||||
else if (pre && *pre) snprintf(dflt, sizeof dflt, "%s/tmp/meow_img_budget", pre);
|
||||
else snprintf(dflt, sizeof dflt, "/tmp/meow_img_budget");
|
||||
g_dir = dflt;
|
||||
}
|
||||
ensure_fixtures();
|
||||
|
||||
p_big = path_join("big.jpg");
|
||||
p_prog = path_join("bigprog.jpg");
|
||||
p_small = path_join("small.jpg");
|
||||
p_png = path_join("big.png");
|
||||
p_bmp = path_join("big.bmp");
|
||||
p_broken = path_join("broken.jpg");
|
||||
p_missing = path_join("no_such_file.jpg");
|
||||
|
||||
if (!file_exists(p_big)) {
|
||||
printf("FATAL 夹具 %s 不存在(先跑 test_meow_img_budget_gen.py)\n", p_big);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- B1 探针 ---- */
|
||||
printf("[B1] 头探测(零像素内存)\n");
|
||||
{
|
||||
int w = 0, h = 0, c = 0;
|
||||
CHECK(meow_img_probe_file(p_big, &w, &h, &c) == 1, "probe 成功");
|
||||
CHECK(w == 3200 && h == 2600, "probe 原始尺寸 3200x2600");
|
||||
CHECK(c == 3, "probe 通道数 3");
|
||||
printf(" probe → %dx%d ch=%d\n", w, h, c);
|
||||
CHECK(meow_img_probe_file(p_broken, NULL, NULL, NULL) == 0, "probe 截断图 → 0");
|
||||
CHECK(meow_img_probe_file(p_missing, NULL, NULL, NULL) == 0, "probe 不存在 → 0");
|
||||
CHECK(meow_img_probe_file(NULL, NULL, NULL, NULL) == 0, "probe NULL → 0");
|
||||
}
|
||||
|
||||
/* ---- B2 小图不动 ---- */
|
||||
printf("[B2] 小图不动 / maxdim<=0 等价旧入口\n");
|
||||
{
|
||||
meow_img *a = meow_img_load_file(p_small);
|
||||
meow_img *b = meow_img_load_file_dim(p_small, 64);
|
||||
meow_img *z = meow_img_load_file_dim(p_small, 0);
|
||||
CHECK(a && b && z, "小图三条路径都成功");
|
||||
CHECK(meow_img_w(b) == meow_img_w(a) && meow_img_h(b) == meow_img_h(a),
|
||||
"maxdim=64 → 尺寸不变");
|
||||
CHECK(b && a && meow_img_w(a) * meow_img_h(a) > 0 &&
|
||||
memcmp(meow_img_pixels(a), meow_img_pixels(b),
|
||||
(size_t)meow_img_w(a) * meow_img_h(a) * 4) == 0,
|
||||
"小图逐字节与旧入口一致");
|
||||
CHECK(z && a && memcmp(meow_img_pixels(a), meow_img_pixels(z),
|
||||
(size_t)meow_img_w(a) * meow_img_h(a) * 4) == 0,
|
||||
"maxdim=0 等价旧入口(逐字节)");
|
||||
meow_img_free(a); meow_img_free(b); meow_img_free(z);
|
||||
}
|
||||
|
||||
/* ---- B3 长边约束 ---- */
|
||||
printf("[B3] 8.32MP baseline JPEG 长边约束\n");
|
||||
{
|
||||
static const int dims[] = { 480, 300, 128, 64 };
|
||||
int wi = 0, hi = 0;
|
||||
meow_img *full = meow_img_load_file(p_big);
|
||||
CHECK(full != NULL, "旧入口解码成功");
|
||||
if (full) { wi = meow_img_w(full); hi = meow_img_h(full); }
|
||||
printf(" 原图 %dx%d (%.2f MP)\n", wi, hi, (double)wi * hi / 1e6);
|
||||
for (size_t i = 0; i < sizeof dims / sizeof dims[0]; i++) {
|
||||
meow_img *s = meow_img_load_file_dim(p_big, dims[i]);
|
||||
char name[96];
|
||||
snprintf(name, sizeof name, "maxdim=%d → 两维 ≤ maxdim 且非空", dims[i]);
|
||||
CHECK(s && meow_img_w(s) <= dims[i] && meow_img_h(s) <= dims[i] && nonblank(s), name);
|
||||
if (s) {
|
||||
double ar0 = (double)wi / hi, ar1 = (double)meow_img_w(s) / meow_img_h(s);
|
||||
CHECK(ar1 > ar0 * 0.98 && ar1 < ar0 * 1.02, "长宽比保持(±2%)");
|
||||
printf(" maxdim=%-4d → %dx%d\n", dims[i], meow_img_w(s), meow_img_h(s));
|
||||
}
|
||||
meow_img_free(s);
|
||||
}
|
||||
meow_img_free(full);
|
||||
}
|
||||
|
||||
/* ---- B4 峰值对比(JPEG) ---- */
|
||||
printf("[B4] 瞬时分配峰值:旧入口 vs 新入口(同一 8.32MP JPEG)\n");
|
||||
{
|
||||
size_t base = peak_reset();
|
||||
meow_img *old = meow_img_load_file(p_big);
|
||||
peak_old = peak_since(base);
|
||||
|
||||
base = peak_reset();
|
||||
meow_img *nw = meow_img_load_file_dim(p_big, 480);
|
||||
peak_new = peak_since(base);
|
||||
|
||||
CHECK(old && nw, "两条路径都成功");
|
||||
peak_report("旧入口 meow_img_load_file", peak_old);
|
||||
peak_report("新入口 _dim(480)", peak_new);
|
||||
printf(" 整幅 RGBA 理论值 = %.2f MiB;新/旧 = %.2f\n",
|
||||
(double)meow_img_w(old) * meow_img_h(old) * 4 / (1024.0 * 1024.0),
|
||||
(double)peak_new / (double)peak_old);
|
||||
CHECK(peak_new * 2 < peak_old, "新入口峰值 < 旧入口 1/2(不物化整幅 RGBA)");
|
||||
CHECK(peak_new < (size_t)20 * 1024 * 1024, "新入口峰值 < 20 MiB(8.32MP 页可加载)");
|
||||
|
||||
/* B5 内容正确性:新 vs 旧整幅 + downscale 精修 */
|
||||
if (old && nw) {
|
||||
meow_img *ref = meow_img_downscale(old, 480);
|
||||
double d = mean_abs_diff(nw, ref ? ref : old, 64);
|
||||
printf(" 与「旧入口+downscale」网格平均差 = %.2f\n", d);
|
||||
CHECK(d < 20.0, "缩解内容结构一致(平均差 < 20)");
|
||||
meow_img_free(ref);
|
||||
}
|
||||
meow_img_free(old);
|
||||
meow_img_free(nw);
|
||||
}
|
||||
|
||||
/* ---- B4b 极端 maxdim ---- */
|
||||
printf("[B4b] maxdim=100(8 倍缩解档位 + 精修)\n");
|
||||
{
|
||||
size_t base = peak_reset();
|
||||
meow_img *s = meow_img_load_file_dim(p_big, 100);
|
||||
size_t peak = peak_since(base);
|
||||
CHECK(s && meow_img_w(s) <= 100 && meow_img_h(s) <= 100 && nonblank(s), "结果 ≤100 且非空");
|
||||
peak_report("_dim(100)", peak);
|
||||
CHECK(peak * 2 < peak_old, "峰值仍 < 旧入口 1/2");
|
||||
meow_img_free(s);
|
||||
}
|
||||
|
||||
/* ---- B4c finding 原始场景 ---- */
|
||||
printf("[B4c] finding 原始场景:1600x2400 漫画页\n");
|
||||
{
|
||||
char *p_page = path_join("page.jpg");
|
||||
if (file_exists(p_page)) {
|
||||
size_t base = peak_reset();
|
||||
meow_img *old = meow_img_load_file(p_page);
|
||||
size_t p_old = peak_since(base);
|
||||
base = peak_reset();
|
||||
meow_img *nw = meow_img_load_file_dim(p_page, 720);
|
||||
size_t p_new = peak_since(base);
|
||||
CHECK(old && nw, "1600x2400 两条路径都成功");
|
||||
if (nw)
|
||||
CHECK(meow_img_w(nw) <= 720 && meow_img_h(nw) <= 720 && nonblank(nw),
|
||||
"结果 ≤720 且非空");
|
||||
if (old)
|
||||
printf(" 整幅 RGBA 理论值 = %.2f MiB\n",
|
||||
(double)meow_img_w(old) * meow_img_h(old) * 4 / (1024.0 * 1024.0));
|
||||
peak_report("旧入口 page.jpg", p_old);
|
||||
peak_report("新入口 _dim(720)", p_new);
|
||||
CHECK(p_new < (size_t)10 * 1024 * 1024, "新入口峰值 < 10 MiB(设备 ~14MB 口径内)");
|
||||
meow_img_free(old);
|
||||
meow_img_free(nw);
|
||||
} else {
|
||||
printf(" SKIP page.jpg(夹具缺失)\n");
|
||||
}
|
||||
free(p_page);
|
||||
}
|
||||
|
||||
/* ---- B6 进升级 JPEG ---- */
|
||||
printf("[B6] progressive JPEG(同一缩解路径)\n");
|
||||
{
|
||||
size_t base = peak_reset();
|
||||
meow_img *old = meow_img_load_file(p_prog);
|
||||
size_t p_old = peak_since(base);
|
||||
base = peak_reset();
|
||||
meow_img *nw = meow_img_load_file_dim(p_prog, 480);
|
||||
size_t p_new = peak_since(base);
|
||||
CHECK(old && nw, "进升级两条路径都成功");
|
||||
CHECK(nw && meow_img_w(nw) <= 480 && meow_img_h(nw) <= 480 && nonblank(nw),
|
||||
"进升级结果 ≤480 且非空");
|
||||
peak_report("旧入口 progressive", p_old);
|
||||
peak_report("新入口 _dim(480) progressive", p_new);
|
||||
CHECK(p_new < p_old, "进升级峰值下降(系数阵列仍在扫描期驻留,见报告)");
|
||||
meow_img_free(old);
|
||||
meow_img_free(nw);
|
||||
}
|
||||
|
||||
/* ---- B7 内存入口 ---- */
|
||||
printf("[B7] 内存入口 meow_img_load_mem_dim(CBZ/档案页场景)\n");
|
||||
{
|
||||
size_t len = 0;
|
||||
uint8_t *buf = read_whole(p_big, &len);
|
||||
CHECK(buf != NULL, "读入 8.32MP JPEG 到内存");
|
||||
if (buf) {
|
||||
size_t base = peak_reset();
|
||||
meow_img *old = meow_img_load_mem(buf, (int)len);
|
||||
size_t p_old = peak_since(base);
|
||||
base = peak_reset();
|
||||
meow_img *nw = meow_img_load_mem_dim(buf, (int)len, 480);
|
||||
size_t p_new = peak_since(base);
|
||||
CHECK(old && nw, "内存入口两条路径都成功");
|
||||
CHECK(nw && meow_img_w(nw) <= 480 && nonblank(nw), "内存入口结果 ≤480 且非空");
|
||||
peak_report("旧 meow_img_load_mem", p_old);
|
||||
peak_report("新 meow_img_load_mem_dim", p_new);
|
||||
CHECK(p_new * 2 < p_old, "内存入口峰值 < 旧入口 1/2");
|
||||
meow_img_free(old);
|
||||
meow_img_free(nw);
|
||||
free(buf);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- B8 坏图 ---- */
|
||||
printf("[B8] 坏图容错\n");
|
||||
{
|
||||
size_t len = 0;
|
||||
uint8_t *buf;
|
||||
CHECK(meow_img_load_file_dim(p_broken, 480) == NULL, "截断 JPEG → NULL");
|
||||
CHECK(meow_img_load_file_dim(p_missing, 480) == NULL, "不存在路径 → NULL");
|
||||
CHECK(meow_img_load_file_dim(NULL, 480) == NULL, "NULL 路径 → NULL");
|
||||
buf = read_whole(p_broken, &len);
|
||||
CHECK(buf && meow_img_load_mem_dim(buf, (int)len, 480) == NULL, "截断 JPEG 内存入口 → NULL");
|
||||
free(buf);
|
||||
CHECK(meow_img_load_mem_dim("x", 3, 480) == NULL, "垃圾内存 → NULL");
|
||||
}
|
||||
|
||||
/* ---- B9 PNG / BMP(M23 起也走解码期缩解;只断言尺寸/非空,峰值打印) ---- */
|
||||
printf("[B9] PNG / BMP:尺寸约束成立,峰值记录(M23 逐行/跳行缩解,应远低于旧入口)\n");
|
||||
{
|
||||
struct { const char *tag; const char *path; } fx[] = {
|
||||
{ "PNG", p_png }, { "BMP", p_bmp },
|
||||
};
|
||||
for (size_t i = 0; i < sizeof fx / sizeof fx[0]; i++) {
|
||||
if (!file_exists(fx[i].path)) {
|
||||
printf(" SKIP %s(夹具缺失)\n", fx[i].tag);
|
||||
continue;
|
||||
}
|
||||
{
|
||||
size_t base = peak_reset();
|
||||
meow_img *old = meow_img_load_file(fx[i].path);
|
||||
size_t p_old = peak_since(base);
|
||||
meow_img *nw;
|
||||
char name[96];
|
||||
base = peak_reset();
|
||||
nw = meow_img_load_file_dim(fx[i].path, 480);
|
||||
{
|
||||
size_t p_new = peak_since(base);
|
||||
snprintf(name, sizeof name, "%s _dim(480) 两维 ≤480 且非空", fx[i].tag);
|
||||
CHECK(nw && meow_img_w(nw) <= 480 && meow_img_h(nw) <= 480 && nonblank(nw), name);
|
||||
peak_report(fx[i].tag, p_old);
|
||||
peak_report(" ↑ 新入口", p_new);
|
||||
snprintf(name, sizeof name, "%s 新入口峰值 ≤ 旧入口(不再整幅物化)", fx[i].tag);
|
||||
CHECK(p_new <= p_old, name);
|
||||
if (old) printf(" %s 原图 %dx%d\n", fx[i].tag, meow_img_w(old), meow_img_h(old));
|
||||
meow_img_free(nw);
|
||||
}
|
||||
meow_img_free(old);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (g_fail) printf("\nRESULT: FAIL (%d)\n", g_fail);
|
||||
else printf("\nRESULT: ALL PASS\n");
|
||||
free(p_big); free(p_prog); free(p_small); free(p_png);
|
||||
free(p_bmp); free(p_broken); free(p_missing);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""test_meow_img_budget_gen.py — 为宿主测试 test_meow_img_budget.c 生成图片夹具。
|
||||
|
||||
用法:python3 test_meow_img_budget_gen.py <输出目录>
|
||||
依赖:Pillow(PIL)。夹具只含结构化图案(白底/黑线/灰块),体积极小。
|
||||
|
||||
生成:
|
||||
big.jpg 3200x2600 baseline 4:2:0 JPEG(8.32MP,峰值对比主用例)
|
||||
bigprog.jpg 3200x2600 progressive JPEG(验证进升级解码走同一缩解路径)
|
||||
page.jpg 1600x2400 baseline JPEG(finding 里 15.4MB 的漫画页原尺寸)
|
||||
small.jpg 64x64 baseline JPEG(「小图不动」用例)
|
||||
big.png 2400x1600 RGB PNG(stb 无解码期缩放:峰值如实记录,不作改善断言)
|
||||
big.bmp 2400x1600 24bpp BMP(同上)
|
||||
broken.jpg JPEG 魔数 + 垃圾字节(头部解析失败,坏图用例)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
def page_pattern(w, h):
|
||||
"""漫画页近似:白底 + 分镜框线 + 灰阶网点带。内容稀疏 → JPEG 体积小。"""
|
||||
im = Image.new("RGB", (w, h), (250, 249, 245))
|
||||
d = ImageDraw.Draw(im)
|
||||
for k in range(0, min(w, h) - 400, 60):
|
||||
d.rectangle([100 + k, 100 + k, 400 + k, 300 + k], outline=(12, 12, 14), width=5)
|
||||
for band in range(14):
|
||||
y0 = band * h // 14
|
||||
for x in range(0, w, 9):
|
||||
d.line([(x, y0), (x + 4, y0 + 7)], fill=(110, 110, 116))
|
||||
return im
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print(__doc__)
|
||||
return 2
|
||||
out = sys.argv[1]
|
||||
os.makedirs(out, exist_ok=True)
|
||||
|
||||
big = page_pattern(3200, 2600)
|
||||
big.save(os.path.join(out, "big.jpg"), "JPEG", quality=55, subsampling=2)
|
||||
big.save(os.path.join(out, "bigprog.jpg"), "JPEG", quality=55, subsampling=2,
|
||||
progressive=True)
|
||||
|
||||
small = page_pattern(64, 64)
|
||||
small.save(os.path.join(out, "small.jpg"), "JPEG", quality=80, subsampling=2)
|
||||
|
||||
page = page_pattern(1600, 2400)
|
||||
page.save(os.path.join(out, "page.jpg"), "JPEG", quality=55, subsampling=2)
|
||||
|
||||
mid = page_pattern(2400, 1600)
|
||||
mid.save(os.path.join(out, "big.png"), "PNG")
|
||||
mid.save(os.path.join(out, "big.bmp"), "BMP")
|
||||
|
||||
# 坏图:JPEG 魔数 + 垃圾字节 → 头部解析失败(stb 对「截断但头部完整」很宽容,不能只截断)
|
||||
garbage = bytes((i * 37 + 11) & 0xFF for i in range(256))
|
||||
with open(os.path.join(out, "broken.jpg"), "wb") as f:
|
||||
f.write(b"\xff\xd8\xff\xe0" + garbage)
|
||||
|
||||
for name in ("big.jpg", "bigprog.jpg", "page.jpg", "small.jpg", "big.png",
|
||||
"big.bmp", "broken.jpg"):
|
||||
print(" %-12s %8d B" % (name, os.path.getsize(os.path.join(out, name))))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -10,14 +10,16 @@
|
||||
* C7 set_colors 注入/回退(lyric_view 层):注入 derive.lyr → cur=fg、gray 随 cur;
|
||||
* NULL 回退 meow_theme 默认
|
||||
*
|
||||
* 编译(在 patch_v2/sdk 下;链法同 test_meow_lyric_view + 单链 meow_img_color.c):
|
||||
* 编译(在 sdk 目录下;链法同 test_meow_lyric_view + 单链 meow_img_color.c;
|
||||
* meow_ui_time.c 提供 meow_ui_time_ms,meow_jpeg.c/meow_png_stream.c 为 M23 解码后端,-lz 供其 inflate):
|
||||
* gcc -Wall -Wextra -o test_meow_img_color test_meow_img_color.c test_host_stub.c \
|
||||
* meow_img_color.c meow_lyric.c meow_lyric_view.c \
|
||||
* meow_img_color.c meow_lyric.c meow_lyric_view.c meow_ui_time.c \
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_img.c meow_jpeg.c meow_png_stream.c meow_ui_media.c meow_notify_ipc.c \
|
||||
* stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm -lz
|
||||
* 运行:./test_meow_img_color
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -2,7 +2,7 @@
|
||||
* test_meow_lyric.c — meow_lyric_* 宿主单测(2026-09-03,SDK 通用歌词组件批1)。
|
||||
*
|
||||
* 编译(宿主,meow_lyric 为纯 libc + pthread,无需 SDK 其它对象):
|
||||
* gcc -Wall -Wextra -pthread -o test_meow_lyric test_meow_lyric.c meow_lyric.c -I.
|
||||
* gcc -Wall -Wextra -pthread -o test_meow_lyric test_meow_lyric.c meow_lyric.c -I. -Iinclude
|
||||
* ./test_meow_lyric
|
||||
*
|
||||
* 覆盖矩阵:
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_lyric_view
|
||||
*
|
||||
* 覆盖矩阵(对齐任务批2:活动行纯白/灰阶、翻译行占位不叠行、偏上锚定、折行中西文、点行 seek 命中、
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_md3
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/* test_meow_time.c — 宿主单测:SDK 时钟换算(meow_ui_time.c / meow_time_ms_from_parts)。
|
||||
*
|
||||
* 背景(M11/M21):mipsel 真机上 meow_ui_time_ms() 曾返回「低 32 位=秒×1000、高 32 位随机」,
|
||||
* 根因是 zig musl 头(time_t 64 位)与 libc 实现(o32 time32 取数)ABI 不一致,见
|
||||
* sdk/CLOCK_ABI.md。修复后取数统一走 meow_time_ms_from_parts(sec, nsec),本测试把
|
||||
* 换算边界(毫秒进位/截断/大秒数/负值回绕)与「真时钟单调且前进」固定在宿主上。
|
||||
*
|
||||
* 注意:宿主(非 mips)编译时 meow_ui_time.c 走 MEOW_TIME32_KERNEL_ABI=0 分支,即普通
|
||||
* clock_gettime —— 本测试**不能**证明 mipsel o32 路径正确(那条路径只靠反汇编证据)。
|
||||
*
|
||||
* 编译(在 sdk/ 目录下):
|
||||
* gcc -Os -Wall -Wextra -I. -Iinclude -c meow_ui_time.c -o meow_ui_time_host.o
|
||||
* gcc -Os -Wall -Wextra -I. -Iinclude test_meow_time.c meow_ui_time_host.o -o test_meow_time
|
||||
* 运行:./test_meow_time (exit 0=PASS / 1=FAIL)
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
|
||||
static int g_fail = 0;
|
||||
static unsigned long g_checks = 0;
|
||||
|
||||
static void eq_ms(const char *name, int64_t sec, int64_t nsec, uint64_t want)
|
||||
{
|
||||
uint64_t got = meow_time_ms_from_parts(sec, nsec);
|
||||
g_checks++;
|
||||
if (got != want) {
|
||||
printf("FAIL %s: sec=%lld nsec=%lld got=%llu want=%llu\n",
|
||||
name, (long long)sec, (long long)nsec,
|
||||
(unsigned long long)got, (unsigned long long)want);
|
||||
g_fail = 1;
|
||||
} else {
|
||||
printf("ok %s = %llu\n", name, (unsigned long long)got);
|
||||
}
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* ---- 1) 换算边界:毫秒 = sec*1000 + nsec/1e6(整除截断) ---- */
|
||||
eq_ms("零", 0, 0, 0);
|
||||
eq_ms("(sec,0) 整秒", 1, 0, 1000);
|
||||
eq_ms("999999ns 截断", 0, 999999, 0);
|
||||
eq_ms("1ms 进位", 0, 1000000, 1);
|
||||
eq_ms("1ns 截断", 0, 1, 0);
|
||||
eq_ms("开机 1135.008s", 1135, 8000000, 1135008);
|
||||
eq_ms("跨秒 1.999999999s", 1, 999999999, 1999);
|
||||
eq_ms("开机 1204.999999999s", 1204, 999999999, 1204999);
|
||||
eq_ms("接近 2038", 2147483647, 999999999, 2147483647999ULL);
|
||||
|
||||
/* ---- 2) 负值:from_parts 只做无符号升位((uint64_t)sec*1000 按二补回绕),不钳制。
|
||||
* 真正把内核 time32 的负 sec 钳成 0 的是 meow_ui_time.c 的 time32_clamp(),它只在
|
||||
* mipsel 分支编译、宿主不可达;这里把 from_parts 的回绕语义钉住,防止有人悄悄把它
|
||||
* 理解成「负值 = 0」而改掉换算。 ---- */
|
||||
eq_ms("负 sec 回绕(-1s)", -1, 0, 0xFFFFFFFFFFFFFC18ULL);
|
||||
eq_ms("负 sec+1ms 回绕", -1, 1000000, 0xFFFFFFFFFFFFFC19ULL);
|
||||
|
||||
/* ---- 3) 真时钟:非零、单调不减、量级合理(不上溢成「高 32 随机」) ---- */
|
||||
uint64_t prev = meow_ui_time_ms();
|
||||
g_checks++;
|
||||
if (prev == 0) {
|
||||
printf("FAIL 时钟返回 0(clock_gettime 失败?)\n");
|
||||
g_fail = 1;
|
||||
}
|
||||
g_checks++;
|
||||
if (prev >= (1ULL << 40)) {
|
||||
printf("FAIL 时钟量级异常(>=2^40 ms):%llu\n", (unsigned long long)prev);
|
||||
g_fail = 1;
|
||||
}
|
||||
/* 有界采样(避免时钟停走时测试挂死):要求单调不减且确实前进 */
|
||||
int advanced = 0;
|
||||
for (int i = 0; i < 200000 && !advanced; i++) {
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
if (now < prev) {
|
||||
printf("FAIL 单调倒退:%llu -> %llu\n",
|
||||
(unsigned long long)prev, (unsigned long long)now);
|
||||
g_fail = 1;
|
||||
break;
|
||||
}
|
||||
advanced = now > prev;
|
||||
prev = now;
|
||||
}
|
||||
g_checks++;
|
||||
if (!advanced) {
|
||||
printf("FAIL 时钟未前进(200000 次采样内无变化)\n");
|
||||
g_fail = 1;
|
||||
} else {
|
||||
printf("ok 时钟前进且单调\n");
|
||||
}
|
||||
|
||||
/* ---- 4) 动画常用差值:两次取样的差 = 毫秒(不能是 0 恒等/整千) ---- */
|
||||
uint64_t t0 = meow_ui_time_ms();
|
||||
int waited = 0;
|
||||
for (int i = 0; i < 200000 && !waited; i++)
|
||||
waited = meow_ui_time_ms() != t0;
|
||||
uint64_t t1 = meow_ui_time_ms();
|
||||
g_checks++;
|
||||
if (!waited || t1 <= t0 || t1 - t0 > 5000) {
|
||||
printf("FAIL 取样差值异常:%llu -> %llu\n",
|
||||
(unsigned long long)t0, (unsigned long long)t1);
|
||||
g_fail = 1;
|
||||
} else {
|
||||
printf("ok 取样差值 %llu ms\n", (unsigned long long)(t1 - t0));
|
||||
}
|
||||
|
||||
printf(g_fail ? "== FAIL ==\n" : "== PASS ==\n");
|
||||
printf("checks=%lu\n", g_checks);
|
||||
return g_fail;
|
||||
}
|
||||
@@ -7,8 +7,8 @@
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_anim
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_box
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* test_meow_ui_config.c — meow_config_* 宿主单测(2026-08-30)。
|
||||
*
|
||||
* 编译(宿主,config 为纯 libc,无需 SDK 其它对象):
|
||||
* gcc -Wall -Wextra test_meow_ui_config.c meow_ui_config.c -o test_meow_ui_config -lpthread
|
||||
* gcc -Wall -Wextra -I. -Iinclude test_meow_ui_config.c meow_ui_config.c -o test_meow_ui_config -lpthread
|
||||
*
|
||||
* 两种模式(meow_config_dir 经 pthread_once 缓存,目录只在首次调用确定,故分两次跑):
|
||||
* ./test_meow_ui_config env → 设 MEOW_DATA_DIR=<mkdtemp>,验证持久目录读写
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_dispatch (无设备,纯宿主)
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_home_gesture
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_iconbtn
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_lazygrid.c meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_lazygrid.c meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_lazygrid
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_listvirt
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
+225
-13
@@ -7,8 +7,8 @@
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_scroll
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
@@ -45,17 +45,6 @@ static meow_widget *make_list(void)
|
||||
return lv;
|
||||
}
|
||||
|
||||
/* 喂一串向下(正)速度采样,模拟快速下划 */
|
||||
static void feed_drag(meow_scroll *s, int step, int n)
|
||||
{
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
for (int i = 0; i < n; i++) {
|
||||
meow_scroll_track(s, step, now + (uint64_t)i * 16);
|
||||
}
|
||||
/* 最后用一个真实稍晚时刻的 track 让采样时间戳真实(保持一致钟) */
|
||||
meow_scroll_track(s, 0, meow_ui_time_ms());
|
||||
}
|
||||
|
||||
static void pump_until_idle(meow_scroll *s, int *max_off, int *mono_ok)
|
||||
{
|
||||
int prev = meow_scroll_offset(s);
|
||||
@@ -169,15 +158,238 @@ static void test_stop(void)
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* ============ 池槽归还回归(M19)============
|
||||
* meow_scroll_wrap 从 MEOW_SCROLL_POOL(=8) 个 wrap 上下文槽里占一个;旧实现 free 只归还
|
||||
* 会话槽、不归还 wrap 槽 → 进程内最多成功 wrap 8 次,之后恒 NULL(图库每次 load_dir 重建,
|
||||
* 进 8 个目录后列表丢惯性)。以下用例覆盖:长循环回收、free 后重建、交错不串槽、
|
||||
* 池满仍返回 NULL、直接 create 不误清槽、free(NULL)/二次 free 安全。 */
|
||||
#define WRAP_POOL 8 /* = meow_ui_scroll.c 的 MEOW_SCROLL_POOL(内部常量,测试按契约固定) */
|
||||
|
||||
/* 连续 64 次 wrap/free:每次都要拿到可用会话,且绑定当前列表 */
|
||||
static void test_wrap_free_recycle(void)
|
||||
{
|
||||
meow_widget *lv = make_list();
|
||||
int ok = 0, first_fail = -1;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||||
if (!s) { if (first_fail < 0) first_fail = i; continue; }
|
||||
meow_list_set_scroll(lv, 0);
|
||||
meow_scroll_track(s, 7, meow_ui_time_ms());
|
||||
if (meow_scroll_offset(s) == 7) ok++;
|
||||
else if (first_fail < 0) first_fail = i; /* 串槽:set/get 不指向本列表 */
|
||||
meow_scroll_free(s);
|
||||
}
|
||||
CHECK(ok == 64, "64 wrap/free cycles all usable, ok=%d first_fail=%d", ok, first_fail);
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* wrap → free → 同一列表再 wrap:必须重新可用 */
|
||||
static void test_wrap_after_free(void)
|
||||
{
|
||||
meow_widget *lv = make_list();
|
||||
meow_scroll *s = meow_scroll_wrap(lv, 0);
|
||||
CHECK(s != NULL, "wrap#1 same list");
|
||||
meow_scroll_free(s);
|
||||
s = meow_scroll_wrap(lv, 0);
|
||||
CHECK(s != NULL, "wrap#2 same list after free");
|
||||
if (s) {
|
||||
meow_list_set_scroll(lv, 0);
|
||||
meow_scroll_track(s, 12, meow_ui_time_ms());
|
||||
CHECK(meow_scroll_offset(s) == 12, "re-wrapped scroll works got %d", meow_scroll_offset(s));
|
||||
meow_scroll_free(s);
|
||||
}
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* 8 个同时活跃 → 第 9 个仍返回 NULL(池满不越界);释放其一的槽立刻可再用 */
|
||||
static void test_wrap_pool_full(void)
|
||||
{
|
||||
meow_widget *lv[WRAP_POOL];
|
||||
meow_scroll *s[WRAP_POOL];
|
||||
int ok = 0;
|
||||
for (int i = 0; i < WRAP_POOL; i++) {
|
||||
lv[i] = make_list();
|
||||
s[i] = meow_scroll_wrap(lv[i], 0);
|
||||
if (s[i]) ok++;
|
||||
}
|
||||
CHECK(ok == WRAP_POOL, "all %d wraps active, ok=%d", WRAP_POOL, ok);
|
||||
|
||||
meow_widget *extra_lv = make_list();
|
||||
meow_scroll *extra = meow_scroll_wrap(extra_lv, 0);
|
||||
CHECK(extra == NULL, "9th simultaneous wrap returns NULL (pool full), got %p", (void *)extra);
|
||||
|
||||
/* 释放中间一个 → 槽立即回收;活着的实例仍各自绑定自己的列表 */
|
||||
meow_scroll_free(s[3]);
|
||||
s[3] = NULL;
|
||||
meow_scroll *again = meow_scroll_wrap(extra_lv, 0);
|
||||
CHECK(again != NULL, "wrap succeeds right after freeing one slot");
|
||||
if (again) {
|
||||
meow_list_set_scroll(extra_lv, 0);
|
||||
meow_scroll_track(again, 9, meow_ui_time_ms());
|
||||
CHECK(meow_scroll_offset(again) == 9, "new wrap bound to its own list got %d",
|
||||
meow_scroll_offset(again));
|
||||
meow_list_set_scroll(lv[5], 33);
|
||||
CHECK(meow_scroll_offset(s[5]) == 33, "other live wrap keeps its own list got %d",
|
||||
meow_scroll_offset(s[5]));
|
||||
meow_scroll_free(again);
|
||||
}
|
||||
for (int i = 0; i < WRAP_POOL; i++) {
|
||||
if (s[i]) meow_scroll_free(s[i]);
|
||||
meow_widget_free(lv[i]);
|
||||
}
|
||||
meow_widget_free(extra_lv);
|
||||
}
|
||||
|
||||
/* 交错:两个 wrap 活跃 → free 其一 → 再 wrap,三者不串槽 */
|
||||
static void test_wrap_interleaved(void)
|
||||
{
|
||||
meow_widget *a = make_list(), *b = make_list(), *c = make_list();
|
||||
meow_scroll *sa = meow_scroll_wrap(a, 0);
|
||||
meow_scroll *sb = meow_scroll_wrap(b, 0);
|
||||
CHECK(sa != NULL && sb != NULL, "two wraps (interleaved) a=%p b=%p", (void *)sa, (void *)sb);
|
||||
if (!sa || !sb) {
|
||||
meow_scroll_free(sa); meow_scroll_free(sb);
|
||||
meow_widget_free(a); meow_widget_free(b); meow_widget_free(c);
|
||||
return;
|
||||
}
|
||||
meow_list_set_scroll(a, 11);
|
||||
meow_list_set_scroll(b, 22);
|
||||
CHECK(meow_scroll_offset(sa) == 11 && meow_scroll_offset(sb) == 22,
|
||||
"distinct ctx a=%d b=%d", meow_scroll_offset(sa), meow_scroll_offset(sb));
|
||||
|
||||
meow_scroll_free(sa);
|
||||
CHECK(meow_scroll_offset(sb) == 22, "remaining wrap intact after freeing first got %d",
|
||||
meow_scroll_offset(sb));
|
||||
|
||||
meow_scroll *sc = meow_scroll_wrap(c, 0);
|
||||
CHECK(sc != NULL, "third wrap after freeing first");
|
||||
if (sc) {
|
||||
meow_list_set_scroll(c, 0);
|
||||
meow_scroll_track(sc, 5, meow_ui_time_ms());
|
||||
CHECK(meow_scroll_offset(sc) == 5, "third wrap bound to its own list got %d",
|
||||
meow_scroll_offset(sc));
|
||||
CHECK(meow_scroll_offset(sb) == 22, "second wrap not clobbered by third got %d",
|
||||
meow_scroll_offset(sb));
|
||||
meow_scroll_free(sc);
|
||||
}
|
||||
meow_scroll_free(sb);
|
||||
meow_widget_free(a); meow_widget_free(b); meow_widget_free(c);
|
||||
}
|
||||
|
||||
/* ---- 直接 create(非 wrap)的回调:用于验证 free 不误清别人的 wrap 槽 ---- */
|
||||
static int g_direct_off;
|
||||
static void direct_set(void *c, int off) { (void)c; g_direct_off = off; }
|
||||
static int direct_max(void *c) { (void)c; return 1000; }
|
||||
static int direct_get(void *c) { (void)c; return g_direct_off; }
|
||||
|
||||
static void test_direct_create_free_keeps_wrap_slot(void)
|
||||
{
|
||||
meow_widget *lv = make_list();
|
||||
meow_scroll *w = meow_scroll_wrap(lv, 0);
|
||||
CHECK(w != NULL, "wrap before direct create");
|
||||
meow_scroll *d = meow_scroll_create(direct_set, direct_max, direct_get, NULL, NULL);
|
||||
CHECK(d != NULL, "direct create");
|
||||
if (d) {
|
||||
meow_scroll_track(d, 40, meow_ui_time_ms());
|
||||
CHECK(g_direct_off == 40, "direct scroll tracks got %d", g_direct_off);
|
||||
meow_scroll_free(d); /* 非 wrap 实例:不得归还/清除任何 wrap 槽 */
|
||||
}
|
||||
if (w) {
|
||||
meow_list_set_scroll(lv, 0);
|
||||
meow_scroll_track(w, 10, meow_ui_time_ms());
|
||||
CHECK(meow_scroll_offset(w) == 10, "wrap intact after freeing direct instance got %d",
|
||||
meow_scroll_offset(w));
|
||||
meow_scroll_free(w);
|
||||
}
|
||||
meow_widget_free(lv);
|
||||
}
|
||||
|
||||
/* free(NULL) 安全;同一实例二次 free 不崩、不清活着的 wrap 槽 */
|
||||
static void test_wrap_free_null_and_double(void)
|
||||
{
|
||||
meow_scroll_free(NULL); /* 必须安全 */
|
||||
|
||||
meow_widget *a = make_list(), *b = make_list();
|
||||
meow_scroll *sa = meow_scroll_wrap(a, 0);
|
||||
meow_scroll *sb = meow_scroll_wrap(b, 0);
|
||||
CHECK(sa != NULL && sb != NULL, "two wraps (double free) a=%p b=%p", (void *)sa, (void *)sb);
|
||||
if (sb) meow_list_set_scroll(b, 44);
|
||||
if (sa) meow_scroll_free(sa);
|
||||
if (sa) meow_scroll_free(sa); /* 二次 free:重复清零,不得清掉 sb 的 ctx */
|
||||
if (sb) {
|
||||
CHECK(meow_scroll_offset(sb) == 44, "live wrap intact after double free got %d",
|
||||
meow_scroll_offset(sb));
|
||||
meow_scroll_free(sb);
|
||||
}
|
||||
meow_scroll *sc = meow_scroll_wrap(a, 0);
|
||||
CHECK(sc != NULL, "wrap still available after free(NULL)/double free");
|
||||
if (sc) meow_scroll_free(sc);
|
||||
meow_widget_free(a); meow_widget_free(b);
|
||||
}
|
||||
|
||||
/* 虚拟化列表行源(仅需 count 有效,不绘制) */
|
||||
static int lv_src2(int idx, meow_list_item *out, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (idx < 0 || idx >= COUNT) return 0;
|
||||
memset(out, 0, sizeof *out);
|
||||
out->title = "row";
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 槽复用不继承上一个 wrap 的 kind:list → listview → list 交替必须各自生效
|
||||
* (free 时清 lv 标记,wrap 时按参数重设;否则复用槽上的旧 kind 会让 set/get 打空) */
|
||||
static void test_wrap_slot_kind_reset(void)
|
||||
{
|
||||
meow_widget *plain = make_list();
|
||||
meow_widget *vl = meow_listview_create(0, 0, 480, LIST_H, COUNT, lv_src2, NULL,
|
||||
MEOW_LIST_SEL_SINGLE);
|
||||
meow_listview_set_item_h(vl, ITEM_H);
|
||||
|
||||
meow_scroll *s = meow_scroll_wrap(plain, 0);
|
||||
CHECK(s != NULL, "wrap plain list");
|
||||
if (s) meow_scroll_free(s);
|
||||
|
||||
s = meow_scroll_wrap(vl, 1);
|
||||
CHECK(s != NULL, "wrap listview on recycled slot");
|
||||
if (s) {
|
||||
meow_listview_set_scroll(vl, 0);
|
||||
meow_scroll_track(s, 20, meow_ui_time_ms());
|
||||
CHECK(meow_scroll_offset(s) == 20, "listview wrapped scroll works got %d",
|
||||
meow_scroll_offset(s));
|
||||
meow_scroll_free(s);
|
||||
}
|
||||
|
||||
s = meow_scroll_wrap(plain, 0);
|
||||
CHECK(s != NULL, "wrap plain list again on recycled slot");
|
||||
if (s) {
|
||||
meow_list_set_scroll(plain, 0);
|
||||
meow_scroll_track(s, 15, meow_ui_time_ms());
|
||||
CHECK(meow_scroll_offset(s) == 15, "plain list after listview slot reuse got %d",
|
||||
meow_scroll_offset(s));
|
||||
meow_scroll_free(s);
|
||||
}
|
||||
meow_widget_free(vl);
|
||||
meow_widget_free(plain);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { printf("init failed\n"); return 1; }
|
||||
/* 池槽回归先跑:新建进程里第一个动 pool 的用例,便于对照「修复前 8/64、修复后 64/64」 */
|
||||
test_wrap_free_recycle();
|
||||
test_track_follow();
|
||||
test_fling_no_boundary();
|
||||
test_fling_boundary_bounce();
|
||||
test_snap();
|
||||
test_stop();
|
||||
test_wrap_after_free();
|
||||
test_wrap_pool_full();
|
||||
test_wrap_interleaved();
|
||||
test_direct_create_free_keeps_wrap_slot();
|
||||
test_wrap_slot_kind_reset();
|
||||
test_wrap_free_null_and_double();
|
||||
meow_ui_shutdown();
|
||||
printf("scroll: %d pass / %d fail\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_select.c meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_select.c meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_select
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
@@ -114,7 +114,7 @@ static void test_select_highlight(void)
|
||||
/* 单选高亮:cur=2(第 3 行)选中背景 = primaryContainer */
|
||||
const meow_theme *thm = meow_theme_get();
|
||||
int pc_r, pc_g, pc_b; UNPK(thm->primaryContainer, pc_r, pc_g, pc_b);
|
||||
int hx = 6, hy = list_y + 2 * SEL_ROW + SEL_ROW / 2;
|
||||
int hx = 12, hy = list_y + 2 * SEL_ROW + SEL_ROW / 2; /* 容器卡横向内缩 8px(MEOW_LISTV_CARD_MARGIN),探针取卡内 */
|
||||
int r, g, b; rd_px(hx, hy, &r, &g, &b);
|
||||
CHECK(near2(r, pc_r) && near2(g, pc_g) && near2(b, pc_b),
|
||||
"T1 highlight row2 primaryContainer got %02X%02X%02X", r, g, b);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* test_meow_ui_str.c — meow_str_* 宿主单测(2026-09-02,§13.i18n 雏形)。
|
||||
*
|
||||
* 编译(宿主,meow_ui_str 为纯 libc,无需 SDK 其它对象):
|
||||
* gcc -Wall -Wextra -o test_meow_ui_str test_meow_ui_str.c meow_ui_str.c -I.
|
||||
* gcc -Wall -Wextra -o test_meow_ui_str test_meow_ui_str.c meow_ui_str.c -I. -Iinclude
|
||||
*
|
||||
* 两种模式(覆盖表按 locale 懒加载,分两次跑):
|
||||
* ./test_meow_ui_str tmp → unsetenv MEOW_DATA_DIR,仅验证内嵌双语表(无覆盖表)
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_swipe
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
* gcc -o test_meow_ui_theme test_meow_ui_theme.c test_host_stub.c \
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_img.c meow_ui_media.c \
|
||||
* meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
*
|
||||
* 验证要点:四套主题 primary/radius 各不相同(MD3 蓝 / MIUIx 橙 + 更大圆角),
|
||||
* 同 style 下 light/dark 的 surface 成对切换;图形用 MEOW_UI_DUMP 导出 PPM 目检。
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_theme_export
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_meow_ui_view_event
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* 当前夹后音量(含 step 语义:a.volume_step 只决定步进,展示原样不/舍入)。
|
||||
*
|
||||
* 编译(宿主 gcc,mipsel 无关):
|
||||
* gcc -o test_meow_ui_vol test_meow_ui_vol.c meow_ui_vol.c -I.
|
||||
* gcc -o test_meow_ui_vol test_meow_ui_vol.c meow_ui_vol.c -I. -Iinclude
|
||||
*/
|
||||
#include "meow_ui_vol.h"
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
* gcc -o test_meow_ui_widgets test_meow_ui_widgets.c test_host_stub.c \
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_img.c meow_ui_media.c \
|
||||
* meow_notify_ipc.c stb_image.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:MEOW_UI_DUMP=/tmp/b2.ppm ./test_meow_ui_widgets (可选,dump 屏幕)
|
||||
*
|
||||
* 注:W_LISTVIEW 的 MEOW_LIST_ICON_IMG 分支调用 meow_ui_img_scaled → 宿主链 meow_ui_img.c+stb_image.c+-lm。
|
||||
|
||||
+2
-1
@@ -4,8 +4,9 @@
|
||||
* (MEOW_NET_SOCK 指向测试 socket)验证:
|
||||
* - meow_net_policy_check(host):按 MEOW_LOG_APP 实体判定 allow/block(netmgrd 决策权威);
|
||||
* - meow_net_report(rx,tx):best-effort 上报(netmgrd 记入 entity_bytes)。
|
||||
* 用法:cc -Iinclude -I. -I../meow_launcherd test_net_ipc.c meow_net_ipc.c -o test_net_ipc
|
||||
* 用法:cc -Iinclude -I. test_net_ipc.c meow_net_ipc.c -o test_net_ipc
|
||||
* 然后 MEOW_NET_SOCK=<sock> MEOW_LOG_APP=<pkg> ./test_net_ipc
|
||||
* 注:需运行中的 netmgrd;缺 netmgrd 时本测试无断言恒退 0,不能作为通过证据。
|
||||
*/
|
||||
#include "include/meow_ui_net.h"
|
||||
#include <stdio.h>
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/ime/media/notify_ipc + 打桩 font/global):
|
||||
* gcc -Wall -Wextra -o test_notif_gesture test_notif_gesture.c test_host_stub.c \
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_ime.c meow_ui_str.c meow_ui_media.c meow_notify_ipc.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_ime.c meow_ui_str.c meow_ui_activity.c meow_ui_media.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_notify_ipc.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_notif_gesture
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* gcc -Wall -o test_notif_open test_notif_open.c test_host_stub.c \
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_notify_ipc.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* 运行:./test_notif_open
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
*
|
||||
* 编译(宿主 gcc):
|
||||
* gcc -o test_notify_integration test_notify_integration.c \
|
||||
* meow_ui_notify.c meow_notify_ipc.c -I. -I../meow_launcherd -lpthread
|
||||
* meow_ui_notify.c meow_notify_ipc.c -I. -Iinclude -lpthread
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_notify_ipc.h"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
*
|
||||
* 编译(宿主 gcc,不依赖 meow_ui.a 其余 TU):
|
||||
* gcc -o test_notify_ipc test_notify_ipc.c meow_notify_ipc.c \
|
||||
* -I. -I../meow_launcherd -lpthread
|
||||
* -I. -Iinclude -lpthread
|
||||
* 运行:./test_notify_ipc (全部 PASS 退出码 0,任一 FAIL 退出码 1)
|
||||
*/
|
||||
#include "meow_notify_ipc.h"
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
* 验证:SDK meow_notify_ipc_send() 产出的帧被 meow_notif_post() 真实落库并回分配 nid>0;
|
||||
* 随后 meow_notify_ipc_dismiss() 经 meow_notif_dismiss() 撤下(count 归 0)。
|
||||
*
|
||||
* 编译(宿主 gcc):
|
||||
* 编译(宿主 gcc;**需主仓组件,公开仓跳过**——../meow_launcherd/meow_notif.c 未随本仓发布):
|
||||
* gcc -o test_notify_post test_notify_post.c meow_notify_ipc.c \
|
||||
* ../meow_launcherd/meow_notif.c -I. -I../meow_launcherd -lpthread
|
||||
* ../meow_launcherd/meow_notif.c -I. -Iinclude -I../meow_launcherd -lpthread
|
||||
*/
|
||||
#include "meow_notify_ipc.h"
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
* gcc -Wall -Wextra -o test_overlay_suppress test_overlay_suppress.c test_host_stub.c \
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_ui_draw.c meow_ui_view.c meow_notify_ipc.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_media.c \
|
||||
* meow_notify_ipc.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
|
||||
@@ -15,11 +15,12 @@
|
||||
* P2 list 焦点:命中非 list 区域时释放列表焦点、切页滚动(不再沿用旧焦点继续滚列表)。
|
||||
* P3 list 脏区:W_LIST 绘制已裁剪到视口;滚动(非整行偏移)+选中行不再向标题间隙/底缘下泄漏。
|
||||
*
|
||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log/widget/input/activity/dialog/ime/img/config/
|
||||
* notify/ipc + 打桩 font/global(test_host_stub) + stb_image + hello 拆分层):
|
||||
* 编译(需主仓组件,公开仓跳过):本仓不含 ../hello_app(公开仓为 examples/hello),且本文件
|
||||
* 直接 #include "../hello_app/hello_internal.h";以下路径按主仓布局(打桩 font/global +
|
||||
* stb_image + hello 拆分层):
|
||||
* 1) 单独编译 app.c 并把 main 改名 hello_app_main(否则与本文件 main 冲突):
|
||||
* gcc -c -Wall -Wextra -DMEOW_UI_HOST_TEST -Dmain=hello_app_main \
|
||||
* -I. -I../hello_app ../hello_app/app.c -o app.o
|
||||
* -I. -Iinclude -I../hello_app ../hello_app/app.c -o app.o
|
||||
* 2) 编译本文件 + 其余 hello 模块 + SDK 源 + 打桩,链接 app.o:
|
||||
* gcc -Wall -Wextra -DMEOW_UI_HOST_TEST -o test_p1p2p3_repro test_p1p2p3_repro.c app.o \
|
||||
* test_host_stub.c meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@
|
||||
* gcc -Wall -Wextra -o test_pinyin test_pinyin.c test_host_stub.c \
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_ui_media.c \
|
||||
* meow_notify_ipc.c stb_image.c -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_img.c meow_ui_media.c \
|
||||
* meow_notify_ipc.c stb_image.c meow_ui_nav.c -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* 编译(宿主 gcc,链真实 gfx/sys/theme/vol/log + 打桩 font/global + meow_notify_ipc):
|
||||
* gcc -o test_sb_collapse test_sb_collapse.c test_host_stub.c \
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c meow_notify_ipc.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
* 3. 图标簇+值:fed 数据 → render 依次调用 meow_gfx_blit_icon(电量/音量/蓝牙/WiFi/亮度)
|
||||
* 与 meow_ui_text_right(%n 值);WiFi 已连-50dBm → WIFI_STRONG。
|
||||
*
|
||||
* 编译(宿主 gcc):
|
||||
* gcc -o test_sb_statusbar test_sb_statusbar.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c \
|
||||
* -I. -I../meow_launcherd
|
||||
* 编译(宿主 gcc;测试自带桩与 meow_ui_gfx/meow_ui_log 的符号重复,需 --allow-multiple-definition):
|
||||
* gcc -Wall -Wextra -o test_sb_statusbar test_sb_statusbar.c test_host_stub.c \
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c meow_notify_ipc.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread -lm -Wl,--allow-multiple-definition
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
* gcc -Wall -o test_sdk_fixes test_sdk_fixes.c test_host_stub.c \
|
||||
* meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \
|
||||
* meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \
|
||||
* meow_ui_draw.c meow_ui_view.c meow_notify_ipc.c \
|
||||
* -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread
|
||||
* meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_media.c \
|
||||
* meow_notify_ipc.c meow_ui_nav.c \
|
||||
* -I. -Iinclude -DMEOW_UI_HOST_TEST -pthread
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ui_internal.h"
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*
|
||||
* 编译(宿主 gcc,仅链 vol/media 两个 TU;无需 meow_ui 其它子系统):
|
||||
* gcc -Wall -Wextra -pthread -o test_sdk_input_keys test_sdk_input_keys.c \
|
||||
* meow_ui_vol.c meow_ui_media.c -I. -I../meow_launcherd
|
||||
* meow_ui_vol.c meow_ui_media.c -I. -Iinclude
|
||||
*/
|
||||
#include "meow_ui_vol.h"
|
||||
#include "meow_ui_media.h"
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
* - on_preempt 回调:第二客户端(raw)抢先 → 本 SDK 客户端收到 PREEMPTED,write 返回负值
|
||||
*
|
||||
* 启动被测试 audiod(宿主二进制,FILE sink)。
|
||||
* 编译:cc -I../meow_launcherd -I. -Os -Wall -Wextra test_ui_audio_host.c meow_ui_audio.c -lpthread -o test_ui_audio_host
|
||||
* **需主仓组件,公开仓跳过**:运行需 ../meow_audiod/meow_audiod_host(未随本仓发布)。
|
||||
* 编译:cc -I. -Iinclude -Os -Wall -Wextra test_ui_audio_host.c meow_ui_audio.c -lpthread -lm -o test_ui_audio_host
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
#include "meow_ipc.h"
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* test_umbrella.c — 伞头完整性回归:只 #include "meow_ui.h",抽查各模块代表性声明
|
||||
* (类型 / 函数 / 宏)都存在。头拆分后用来证明伞头聚合无遗漏、签名未变。
|
||||
* 用法:gcc -std=c11 -Wall -Wextra -I<sdk> -c test_umbrella.c -o test_umbrella.o
|
||||
* 用法(在 sdk/ 目录下):gcc -std=c11 -Wall -Wextra -I. -Iinclude -c test_umbrella.c -o test_umbrella.o
|
||||
* (纯声明级检查,不链接、不调用;产生可执行代码仍 0 warning)。
|
||||
*/
|
||||
#include "meow_ui.h"
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# tuku(os.meow.picbrowser)打包与签名
|
||||
|
||||
图库 App(系统包名 `os.meow.picbrowser`)。构建与打包:
|
||||
|
||||
```sh
|
||||
./build_tuku.sh # 交叉编译 picbrowser(MIPS32r2 静态)
|
||||
./pack_tuku.sh # 打包 → os.meow.picbrowser.mpk(根布局;未签名)
|
||||
```
|
||||
|
||||
## 浏览交互(文件管理器模型,M18)
|
||||
|
||||
- 列表 = 当前目录一层的**子目录 + 受支持图片(png/jpg/jpeg/bmp)+ 档案册(.cbz/.zip)**;
|
||||
目录优先,组内自然序(大小写不敏感,同键按原始字节兜底,`A.jpg`/`a.jpg` 不抖动)。
|
||||
- 点目录 = 进入(空目录也进);点图片 = 从该图开始读(按**文件名**交给阅读器,
|
||||
由阅读器的全量目录册定位起始页——列表被 `MR_LIB_SCAN_MAX` 截断时也不会点错图);
|
||||
点档案册 = 打开档案册(第 1 页/续读)。
|
||||
- 单目录超过 2000 项时列表只显示前 2000 项并给出「已截断」提示行;图片仍可进入阅读器翻到。
|
||||
- 缩略图功能已关闭:行内只有类型图标 + 文件名 + 类型·大小;首帧只做单层
|
||||
`opendir/readdir + lstat`,滚动/点击路径不解码、不访问文件、不重排字符串。
|
||||
- 浏览根锁定 TF 卡根(`/usr/data/mnt/sd_0`):根目录不显示返回箭头,左缘返回 = 退出 App;
|
||||
**不允许上溯到 SD 卡的上级目录**。
|
||||
- 根目录不可访问(未插卡/挂载失败)→ 友好空态「未检测到 SD 卡」;点顶栏刷新或重进页面即重扫,
|
||||
不弹错、不崩溃。
|
||||
|
||||
## 一键签名出包
|
||||
|
||||
```sh
|
||||
MEOW_SIGN_KEY=/path/to/signing.key ./pack_tuku.sh
|
||||
```
|
||||
|
||||
- 工具查找顺序:`$MEOW_SIGN` → `PATH` 里的 `meow_sign` → `$HOME/Proj/MeowOS-For-R3P2/patch_v2/meow_sign/meow_sign`。
|
||||
- 私钥:`$MEOW_SIGN_KEY`。未设置时脚本**警告并仍产出未签名包**(退出码 0);但 `os.meow.*` 系统包无签名会被设备拒装。
|
||||
- 有工具 + 密钥时执行 `meow_sign sign <mpk> <key>`;若存在 `<key>.pub` 再执行 `meow_sign verify <mpk> <key>.pub`。
|
||||
- 私钥**不入库、不随包分发**;本仓不包含任何密钥材料。
|
||||
|
||||
## mpk 布局
|
||||
|
||||
`.mpk` = tar.gz,条目直接在根层:
|
||||
|
||||
```
|
||||
os.meow.picbrowser.mpk
|
||||
├── app.json
|
||||
├── picbrowser
|
||||
└── <signature footer> # 仅签名包有(Ed25519,尾部追加)
|
||||
```
|
||||
|
||||
即 `tar -C build/os.meow.picbrowser -czf os.meow.picbrowser.mpk app.json picbrowser`——
|
||||
不带 `os.meow.picbrowser/` 目录前缀,也不带 `./` 前缀。脚本在签名前用 `tar -tzf` 断言这一点。
|
||||
|
||||
签名是往 tar.gz 尾部**追加** footer,签名后的 mpk 不要再跑 `tar -t` / `gzip` 之类的命令:
|
||||
gzip 会报 `trailing garbage ignored`,属预期现象,不是包损坏。要检查布局请在签名前做。
|
||||
|
||||
## 安装
|
||||
|
||||
把 `.mpk` 放到设备 TF 卡,执行 `nyai install os.meow.picbrowser.mpk`;
|
||||
`os.meow.*` 包必须验签通过(footer.package == 文件名茎 == `app.json.package`)。
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
/* app.c — 图库装配层:main + 建页 + meow_ui_run 回调路由 + 主题监听。
|
||||
*
|
||||
* 契约(docs/APP_DEV_GUIDE.md §2-§3):
|
||||
* - main 只做 meow_ui_init → 建 view → meow_ui_run → 清理;退出走 meow_ui_request_exit;
|
||||
* - 页面切换统一走 meow_view_page_push/pop(activity 栈 + on_enter/on_leave 唯一入口);
|
||||
* - 物理键不映射返回/确认(on_key 仅日志);屏底上滑由 SDK 系统手势接管,App 不拦截;
|
||||
* - 左缘返回:reader/settings=pop;lock 页自行消费;书库逐级上移,
|
||||
* **到 TF 卡根=退出 App**(浏览根锁定,不再上溯系统目录;顶栏也没有越根的「上级」)。
|
||||
*
|
||||
* 主题同步:注册 meow_theme_add_listener;明暗/风格族切换时更新页面底色并整帧 invalidate,
|
||||
* 各页 on_draw / SDK 控件下一帧即用新语义色重绘(不缓存颜色)。
|
||||
*/
|
||||
#include "tuku_internal.h"
|
||||
#include "ui/reader/reader.h"
|
||||
#include "meow_ui_nav.h"
|
||||
|
||||
#include <signal.h>
|
||||
|
||||
static void on_sigterm(int s)
|
||||
{
|
||||
(void)s;
|
||||
meow_ui_request_exit();
|
||||
}
|
||||
|
||||
/* 向 launcher 上报当前页栈深/顶层,供系统导航栈跟随。各页 on_enter 调用。 */
|
||||
void mr_nav_report(void)
|
||||
{
|
||||
int depth = meow_ui_activity_depth();
|
||||
if (depth <= 0) depth = 1;
|
||||
int id = meow_ui_activity_active();
|
||||
const char *top = (id == ACT_READER) ? "reader"
|
||||
: (id == ACT_SETTINGS) ? "settings"
|
||||
: (id == ACT_LOCK) ? "lock"
|
||||
: "library";
|
||||
meow_ui_nav_report((uint32_t)depth, top);
|
||||
}
|
||||
|
||||
/* 主题切换:页面底色 + 整帧重绘(控件与 on_draw 全部从 meow_theme_get() 取色)。 */
|
||||
static void app_theme_changed(meow_theme_mode m)
|
||||
{
|
||||
(void)m;
|
||||
meow_ui_set_page_bg(meow_theme_get()->bg);
|
||||
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||||
}
|
||||
|
||||
static int app_active(void)
|
||||
{
|
||||
return meow_ui_activity_active();
|
||||
}
|
||||
|
||||
static void app_on_key(int keycode, int is_down)
|
||||
{
|
||||
(void)keycode; (void)is_down; /* 物理键是系统语义,不做返回/确认映射 */
|
||||
meow_log(MEOW_LOG_DEBUG, "key");
|
||||
}
|
||||
|
||||
static void app_on_tap(int x, int y)
|
||||
{
|
||||
switch (app_active()) {
|
||||
case ACT_LIBRARY: library_on_tap(x, y); break;
|
||||
case ACT_READER: reader_on_tap(x, y); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
static void app_on_swipe(int dir, int dx, int dy)
|
||||
{
|
||||
(void)dy;
|
||||
if (dir == MEOW_SWIPE_LEFT) {
|
||||
if (app_active() == ACT_LIBRARY) {
|
||||
if (!library_go_up()) {
|
||||
meow_log(MEOW_LOG_INFO, "exit:swipe");
|
||||
meow_ui_request_exit();
|
||||
}
|
||||
} else {
|
||||
meow_view_page_pop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* SDK 的 on_swipe 把位移量放在第二参(第三参为 0):UP/DOWN 时 dx=纵向位移。 */
|
||||
if (app_active() == ACT_LIBRARY) library_on_swipe(dir, dx);
|
||||
}
|
||||
|
||||
/* 通用长按(SDK §6.3)是 opt-in:未注册回调时 meow_input_long_press_poll 直接返回 0,
|
||||
* MEOW_EV_LONG_PRESS 永远不会产生 —— 锁屏的「长按退格清空」在真机上就退化成单删一位。
|
||||
* 这里注册一个 no-op 回调把长按打开;事件仍然先经 meow_view_dispatch_event 路由,锁屏页
|
||||
* 自己消费 LONG_PRESS(其它页命中不到就走 no-op)。
|
||||
*
|
||||
* 只在锁屏为顶层页时启用:长按一旦触发,其抬手不再产生 TAP/SWIPE(SDK 抑制点击),
|
||||
* 全程开启会让书库/阅读器里「按住稍久」的点击被吞掉。锁屏页解锁 pop 后由 app_on_tick
|
||||
* 下一拍关掉,避免把长按语义泄漏到其它页。 */
|
||||
static int g_long_press_on;
|
||||
|
||||
static void app_on_long_press(int x, int y)
|
||||
{
|
||||
(void)x; (void)y; /* 锁屏已在 view 分发里消费;其余页没有长按语义 */
|
||||
}
|
||||
|
||||
static void app_sync_long_press(void)
|
||||
{
|
||||
int on = (app_active() == ACT_LOCK);
|
||||
if (on == g_long_press_on) return;
|
||||
meow_ui_set_long_press_cb(on ? app_on_long_press : NULL);
|
||||
g_long_press_on = on;
|
||||
}
|
||||
|
||||
static void app_on_tick(void)
|
||||
{
|
||||
app_sync_long_press();
|
||||
library_on_tick();
|
||||
}
|
||||
|
||||
static void app_on_swipe_progress(int x_start, int dx)
|
||||
{
|
||||
meow_ui_swipe_hint(x_start + dx);
|
||||
}
|
||||
|
||||
/* 跟手拖动路由(M6):ACT_READER 需要原始 down/move 算位移;其余页保持原语义。
|
||||
* 屏底热区/左缘的避让由 reader 侧按同一口径判断(reader_on_touch_down)。 */
|
||||
static void app_on_touch_down(int x, int y)
|
||||
{
|
||||
if (app_active() == ACT_LIBRARY) library_on_touch_down(x, y);
|
||||
else if (app_active() == ACT_READER) reader_on_touch_down(x, y);
|
||||
}
|
||||
|
||||
static void app_on_touch_move(int x, int y)
|
||||
{
|
||||
if (app_active() == ACT_LIBRARY) library_on_touch_move(x, y);
|
||||
else if (app_active() == ACT_READER) reader_on_touch_move(x, y);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
meow_log(MEOW_LOG_INFO, "start");
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) {
|
||||
meow_log(MEOW_LOG_ERROR, "init failed");
|
||||
return 1;
|
||||
}
|
||||
|
||||
meow_theme_add_listener(app_theme_changed);
|
||||
meow_ui_set_page_bg(meow_theme_get()->bg);
|
||||
meow_ui_nav_init();
|
||||
signal(SIGTERM, on_sigterm);
|
||||
|
||||
/* 先建全部页面(on_create 建各自控件/扫描书库),再按启动锁状态进根页。 */
|
||||
meow_view *library = library_view_create();
|
||||
meow_view *reader = reader_view_create();
|
||||
meow_view *settings = settings_view_create();
|
||||
meow_view *lock = lock_view_create();
|
||||
|
||||
meow_view_page_push(lock_view_should_show() ? ACT_LOCK : ACT_LIBRARY);
|
||||
app_sync_long_press(); /* 进主循环前先对齐一次:启动即锁屏时首帧长按就可用 */
|
||||
|
||||
meow_ui_events ev = {
|
||||
app_on_key, app_on_tap, app_on_swipe, app_on_tick,
|
||||
app_on_swipe_progress, app_on_touch_down, app_on_touch_move,
|
||||
};
|
||||
meow_ui_run(&ev);
|
||||
|
||||
meow_view_free(lock);
|
||||
meow_view_free(settings);
|
||||
meow_view_free(reader);
|
||||
meow_view_free(library);
|
||||
meow_ui_shutdown();
|
||||
meow_log(MEOW_LOG_INFO, "exit");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{"name":"图库","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.picbrowser/picbrowser",
|
||||
"package":"os.meow.picbrowser","version":"0.1","version_code":1,"framework":1,
|
||||
"fonts":"common","route.reader.fullscreen":1}
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# build_tuku.sh — 构建图库(静态 MIPS32r2,链 meow_ui.a + libz.a + -lpthread)。
|
||||
#
|
||||
# SDK 路径用绝对路径(默认 $HOME/Proj/MeowOS-SDK/sdk),可用 MEOW_SDK_DIR 覆盖;
|
||||
# 故意不用 ../../sdk —— 交付时 tuku/ 会搬到独立仓 ~/Proj/Meow-OS-APPs/os.meow.picbrowser。
|
||||
# 交叉编译器/sysroot 同 SDK:MEOW_CC / MEOW_SYSROOT 可覆盖。
|
||||
# -I"$SYSROOT/include":zip 容器层(logic/zip/zip.c)直接 include <zlib.h>,
|
||||
# 头文件随 sysroot 的 libz.a 一起提供(SDK 自身只用最小 zlib ABI 自声明)。
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
CC="${MEOW_CC:-$HOME/.tools/zig-mipsel-cc}"
|
||||
SYSROOT="${MEOW_SYSROOT:-$HOME/.tools/mipsel-linux-musleabi-sysroot}"
|
||||
SDK="${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk}"
|
||||
|
||||
"$CC" -Os -static -Wall -Wextra -I"$SDK" -I"$SYSROOT/include" -I. \
|
||||
app.c ui/library/library.c ui/reader/reader.c ui/settings/settings.c ui/lock/lock.c \
|
||||
logic/library.c logic/reader.c logic/reader/page_source.c logic/reader/nav.c \
|
||||
logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c logic/pin.c \
|
||||
"$SDK/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o picbrowser
|
||||
ls -la picbrowser
|
||||
file picbrowser
|
||||
@@ -0,0 +1,462 @@
|
||||
/* logic/library.c — 书库文件管理器模型:单层枚举 + 纯内存行合成(纯 C)。
|
||||
*
|
||||
* 扫描(mr_library_scan)与 os.meow.files 的 fsx_scan_dir 同构:
|
||||
* - 一次 opendir/readdir,逐项 lstat(只认目录、受支持图片、.cbz/.zip;符号链接跳过防环);
|
||||
* - 隐藏项("." 开头,含 . ..)跳过;空目录/无图目录/系统目录照常列出;
|
||||
* - 排序:目录优先,组内按 mr_natural_cmp 自然升序;
|
||||
* - 副标题(类型 · 大小)在扫描期生成一次,渲染期不再做任何格式化/IO;
|
||||
* - 条目到 MR_LIB_SCAN_MAX 后仍有条目 → 截断标注。
|
||||
*
|
||||
* 阅读器辅助(与列表模型解耦):mr_library_dir_has_images 单层判据 +
|
||||
* mr_book_adjacent(_ex) 兄弟册衔接(M7/M11 阅读器翻到相邻册用)。
|
||||
*/
|
||||
#include "library.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#ifdef MR_LIBRARY_TEST_HOOKS
|
||||
static mr_library_io_stats g_io;
|
||||
|
||||
void mr_library_test_io_reset(void)
|
||||
{
|
||||
memset(&g_io, 0, sizeof g_io);
|
||||
}
|
||||
|
||||
void mr_library_test_io_get(mr_library_io_stats *out)
|
||||
{
|
||||
if (out) *out = g_io;
|
||||
}
|
||||
|
||||
#define IO_OPEN() (g_io.opendirs++)
|
||||
#define IO_READDIR() (g_io.readdirs++)
|
||||
#define IO_LSTAT() (g_io.lstats++)
|
||||
#else
|
||||
#define IO_OPEN() ((void)0)
|
||||
#define IO_READDIR() ((void)0)
|
||||
#define IO_LSTAT() ((void)0)
|
||||
#endif
|
||||
|
||||
/* 系统目录不参与**册间衔接**(M7 兄弟册表);浏览本身已放开(不隐藏)。 */
|
||||
static int ignored_dir_name(const char *name)
|
||||
{
|
||||
static const char *const skip[] = { "MeowAPPs", "MeowData", "MeowOS", "lost+found" };
|
||||
if (!name || name[0] == '.') return 1;
|
||||
for (unsigned i = 0; i < sizeof skip / sizeof skip[0]; i++)
|
||||
if (strcmp(name, skip[i]) == 0) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int copy_str(char *dst, int dstsz, const char *src)
|
||||
{
|
||||
if (!dst || dstsz <= 0) return -1;
|
||||
if (!src) { dst[0] = '\0'; return 0; }
|
||||
int n = (int)strlen(src);
|
||||
if (n >= dstsz) return -1;
|
||||
memcpy(dst, src, (size_t)n + 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int join_path(char *out, int outsz, const char *dir, const char *name)
|
||||
{
|
||||
if (!out || outsz <= 0 || !dir || !name) return -1;
|
||||
int n;
|
||||
size_t dl = strlen(dir);
|
||||
if (dl > 0 && dir[dl - 1] == '/')
|
||||
n = snprintf(out, (size_t)outsz, "%s%s", dir, name);
|
||||
else
|
||||
n = snprintf(out, (size_t)outsz, "%s/%s", dir, name);
|
||||
return (n > 0 && n < outsz) ? 0 : -1;
|
||||
}
|
||||
|
||||
int mr_path_is_image(const char *name)
|
||||
{
|
||||
if (!name) return 0;
|
||||
const char *dot = strrchr(name, '.');
|
||||
if (!dot || dot == name) return 0;
|
||||
dot++;
|
||||
return strcasecmp(dot, "png") == 0 || strcasecmp(dot, "jpg") == 0 ||
|
||||
strcasecmp(dot, "jpeg") == 0 || strcasecmp(dot, "bmp") == 0;
|
||||
}
|
||||
|
||||
int mr_path_is_archive(const char *name)
|
||||
{
|
||||
if (!name) return 0;
|
||||
const char *dot = strrchr(name, '.');
|
||||
if (!dot || dot == name) return 0;
|
||||
dot++;
|
||||
return strcasecmp(dot, "cbz") == 0 || strcasecmp(dot, "zip") == 0;
|
||||
}
|
||||
|
||||
int mr_natural_cmp(const char *a, const char *b)
|
||||
{
|
||||
const unsigned char *pa = (const unsigned char *)a;
|
||||
const unsigned char *pb = (const unsigned char *)b;
|
||||
if (!a || !b) return (a == b) ? 0 : (a ? 1 : -1);
|
||||
|
||||
while (*pa && *pb) {
|
||||
if (isdigit(*pa) && isdigit(*pb)) {
|
||||
const unsigned char *za = pa, *zb = pb;
|
||||
while (*pa == '0') pa++;
|
||||
while (*pb == '0') pb++;
|
||||
const unsigned char *da = pa, *db = pb;
|
||||
while (isdigit(*pa)) pa++;
|
||||
while (isdigit(*pb)) pb++;
|
||||
|
||||
size_t la = (size_t)(pa - da), lb = (size_t)(pb - db);
|
||||
if (la != lb) return la < lb ? -1 : 1;
|
||||
if (la > 0) {
|
||||
int c = memcmp(da, db, la);
|
||||
if (c != 0) return c < 0 ? -1 : 1;
|
||||
}
|
||||
size_t oa = (size_t)(da - za), ob = (size_t)(db - zb);
|
||||
if (oa != ob) return oa < ob ? -1 : 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
int ca = tolower(*pa), cb = tolower(*pb);
|
||||
if (ca != cb) return ca < cb ? -1 : 1;
|
||||
pa++;
|
||||
pb++;
|
||||
}
|
||||
if (*pa) return 1;
|
||||
if (*pb) return -1;
|
||||
return strcmp(a, b) < 0 ? -1 : (strcmp(a, b) > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 扫描 / 排序 / 行合成 */
|
||||
/* ============================================================ */
|
||||
static void fmt_size(long long size, char *out, int outsz)
|
||||
{
|
||||
if (size < 0) size = 0;
|
||||
static const char *const unit[] = { "B", "KB", "MB", "GB" };
|
||||
double v = (double)size;
|
||||
int i = 0;
|
||||
while (v >= 1024.0 && i < 3) { v /= 1024.0; i++; }
|
||||
if (i == 0) snprintf(out, (size_t)outsz, "%lld B", size);
|
||||
else snprintf(out, (size_t)outsz, "%.1f %s", v, unit[i]);
|
||||
}
|
||||
|
||||
/* 副标题只在扫描期生成一次(渲染期零格式化)。 */
|
||||
static void format_subtitle(mr_library_entry *e)
|
||||
{
|
||||
char sz[32];
|
||||
switch (e->kind) {
|
||||
case MR_LIB_KIND_DIR:
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "文件夹");
|
||||
break;
|
||||
case MR_LIB_KIND_ARCHIVE:
|
||||
fmt_size(e->size, sz, (int)sizeof sz);
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "档案册 · %s", sz);
|
||||
break;
|
||||
default:
|
||||
fmt_size(e->size, sz, (int)sizeof sz);
|
||||
snprintf(e->subtitle, sizeof e->subtitle, "图片 · %s", sz);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void mr_library_free(mr_library *lib)
|
||||
{
|
||||
if (!lib) return;
|
||||
free(lib->items);
|
||||
lib->items = NULL;
|
||||
lib->count = 0;
|
||||
lib->cap = 0;
|
||||
lib->truncated = 0;
|
||||
}
|
||||
|
||||
static int lib_add(mr_library *lib, const mr_library_entry *e)
|
||||
{
|
||||
if (lib->count >= lib->cap) {
|
||||
int cap = lib->cap ? lib->cap * 2 : 16;
|
||||
mr_library_entry *ni = (mr_library_entry *)realloc(lib->items,
|
||||
(size_t)cap * sizeof *ni);
|
||||
if (!ni) return -1;
|
||||
lib->items = ni;
|
||||
lib->cap = cap;
|
||||
}
|
||||
lib->items[lib->count++] = *e;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 目录优先;组内自然序(大小写不敏感 + 原始字节兜底,结果确定)。 */
|
||||
static int entry_cmp(const void *pa, const void *pb)
|
||||
{
|
||||
const mr_library_entry *a = (const mr_library_entry *)pa;
|
||||
const mr_library_entry *b = (const mr_library_entry *)pb;
|
||||
int ad = (a->kind == MR_LIB_KIND_DIR), bd = (b->kind == MR_LIB_KIND_DIR);
|
||||
if (ad != bd) return ad ? -1 : 1;
|
||||
return mr_natural_cmp(a->name, b->name);
|
||||
}
|
||||
|
||||
static int add_entry(mr_library *lib, const char *name, const char *full,
|
||||
int kind, const struct stat *st)
|
||||
{
|
||||
mr_library_entry e;
|
||||
memset(&e, 0, sizeof e);
|
||||
if (copy_str(e.name, (int)sizeof e.name, name) != 0) return 0;
|
||||
if (copy_str(e.path, (int)sizeof e.path, full) != 0) return 0;
|
||||
e.kind = kind;
|
||||
e.size = (kind == MR_LIB_KIND_DIR) ? 0 : (long long)st->st_size;
|
||||
format_subtitle(&e);
|
||||
return lib_add(lib, &e);
|
||||
}
|
||||
|
||||
int mr_library_scan(mr_library *lib, const char *dir, int max_depth)
|
||||
{
|
||||
(void)max_depth; /* 兼容旧签名:扫描不递归 */
|
||||
if (!lib) return -1;
|
||||
mr_library_free(lib);
|
||||
if (!dir || !dir[0]) return -1;
|
||||
|
||||
IO_OPEN();
|
||||
DIR *d = opendir(dir);
|
||||
if (!d) return -1;
|
||||
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
IO_READDIR();
|
||||
const char *name = de->d_name;
|
||||
if (name[0] == '.') continue; /* . .. 与隐藏项 */
|
||||
|
||||
char full[MR_PATH_MAX];
|
||||
if (join_path(full, (int)sizeof full, dir, name) != 0) continue;
|
||||
|
||||
IO_LSTAT();
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0) continue;
|
||||
if (S_ISLNK(st.st_mode)) continue;
|
||||
|
||||
int kind;
|
||||
if (S_ISDIR(st.st_mode)) kind = MR_LIB_KIND_DIR;
|
||||
else if (!S_ISREG(st.st_mode)) continue;
|
||||
else if (mr_path_is_image(name)) kind = MR_LIB_KIND_IMAGE;
|
||||
else if (mr_path_is_archive(name)) kind = MR_LIB_KIND_ARCHIVE;
|
||||
else continue;
|
||||
|
||||
if (lib->count >= MR_LIB_SCAN_MAX) { lib->truncated = 1; break; }
|
||||
if (add_entry(lib, name, full, kind, &st) != 0) {
|
||||
closedir(d);
|
||||
mr_library_free(lib);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
|
||||
if (lib->count > 1)
|
||||
qsort(lib->items, (size_t)lib->count, sizeof *lib->items, entry_cmp);
|
||||
return lib->count;
|
||||
}
|
||||
|
||||
int mr_library_row_at(const mr_library *lib, int idx, mr_library_row *out)
|
||||
{
|
||||
static char trunc_sub[96];
|
||||
static int trunc_ready;
|
||||
|
||||
if (!lib || !out) return 0;
|
||||
if (idx == lib->count && lib->truncated) {
|
||||
if (!trunc_ready) {
|
||||
/* 截断只是列表显示上限:图片仍能从任一可见图片进入阅读器后翻到,
|
||||
* 文案不要让人误以为「没有更多图片」。 */
|
||||
snprintf(trunc_sub, sizeof trunc_sub,
|
||||
"仅显示前 %d 项(图片可在阅读器中翻到)", MR_LIB_SCAN_MAX);
|
||||
trunc_ready = 1;
|
||||
}
|
||||
memset(out, 0, sizeof *out);
|
||||
out->title = "已截断";
|
||||
out->subtitle = trunc_sub;
|
||||
out->disabled = 1;
|
||||
out->index = -1;
|
||||
return 1;
|
||||
}
|
||||
if (idx < 0 || idx >= lib->count) return 0;
|
||||
|
||||
const mr_library_entry *e = &lib->items[idx];
|
||||
memset(out, 0, sizeof *out);
|
||||
out->title = e->name;
|
||||
out->subtitle = e->subtitle;
|
||||
out->kind = e->kind;
|
||||
out->index = idx;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_path_within_root(const char *path, const char *root)
|
||||
{
|
||||
if (!path || !path[0] || !root || !root[0]) return 0;
|
||||
size_t rl = strlen(root);
|
||||
while (rl > 1 && root[rl - 1] == '/') rl--;
|
||||
if (rl == 1 && root[0] == '/') return path[0] == '/';
|
||||
if (strncmp(path, root, rl) != 0) return 0;
|
||||
char c = path[rl];
|
||||
return c == '\0' || c == '/';
|
||||
}
|
||||
|
||||
int mr_path_parent(const char *path, char *out, int outsz)
|
||||
{
|
||||
if (!path || !out || outsz <= 0) return -1;
|
||||
size_t n = strlen(path);
|
||||
while (n > 1 && path[n - 1] == '/') n--;
|
||||
if (n <= 1) {
|
||||
if (outsz < 2) return -1;
|
||||
out[0] = '/';
|
||||
out[1] = '\0';
|
||||
return 0;
|
||||
}
|
||||
while (n > 0 && path[n - 1] != '/') n--;
|
||||
while (n > 1 && path[n - 1] == '/') n--;
|
||||
if (n >= (size_t)outsz) return -1;
|
||||
memcpy(out, path, n);
|
||||
out[n] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 册间衔接(M7 / M11) */
|
||||
/* ============================================================ */
|
||||
int mr_library_dir_has_images(const char *dir)
|
||||
{
|
||||
if (!dir || !dir[0]) return 0;
|
||||
IO_OPEN();
|
||||
DIR *d = opendir(dir);
|
||||
if (!d) return 0;
|
||||
|
||||
int found = 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
IO_READDIR();
|
||||
const char *name = de->d_name;
|
||||
if (name[0] == '.' || !mr_path_is_image(name)) continue;
|
||||
char full[MR_PATH_MAX];
|
||||
if (join_path(full, (int)sizeof full, dir, name) != 0) continue;
|
||||
IO_LSTAT();
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0 || !S_ISREG(st.st_mode)) continue;
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
closedir(d);
|
||||
return found;
|
||||
}
|
||||
|
||||
static int name_ptr_cmp(const void *pa, const void *pb)
|
||||
{
|
||||
const char *a = *(const char *const *)pa;
|
||||
const char *b = *(const char *const *)pb;
|
||||
return mr_natural_cmp(a, b);
|
||||
}
|
||||
|
||||
/* 兄弟册判据(M11):目录=直接含图;普通文件=档案册且有图片页。
|
||||
* 两者同列,交给 name_ptr_cmp 自然序混排。 */
|
||||
static int sibling_is_book(const char *full, const struct stat *st,
|
||||
mr_lib_archive_fn probe, void *ud)
|
||||
{
|
||||
if (S_ISDIR(st->st_mode)) return mr_library_dir_has_images(full);
|
||||
if (S_ISREG(st->st_mode)) {
|
||||
const char *slash = strrchr(full, '/');
|
||||
const char *name = slash ? slash + 1 : full;
|
||||
if (!mr_path_is_archive(name)) return 0;
|
||||
if (!probe) return 0;
|
||||
int n = probe(full, ud);
|
||||
return n > 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_book_adjacent_ex(const char *dir, int delta, char *out, int outsz,
|
||||
mr_lib_archive_fn archive_pages, void *ud)
|
||||
{
|
||||
if (!out || outsz <= 0) return -1;
|
||||
out[0] = '\0';
|
||||
if (!dir || !dir[0] || (delta != 1 && delta != -1)) return -1;
|
||||
|
||||
char norm[MR_PATH_MAX];
|
||||
size_t dl = strlen(dir);
|
||||
while (dl > 1 && dir[dl - 1] == '/') dl--;
|
||||
if (dl >= sizeof norm) return -1;
|
||||
memcpy(norm, dir, dl);
|
||||
norm[dl] = '\0';
|
||||
|
||||
const char *slash = strrchr(norm, '/');
|
||||
if (!slash || !slash[1]) return -1;
|
||||
const char *cur = slash + 1;
|
||||
|
||||
char parent[MR_PATH_MAX];
|
||||
if (mr_path_parent(norm, parent, (int)sizeof parent) != 0) return -1;
|
||||
|
||||
DIR *d = opendir(parent);
|
||||
if (!d) return 0;
|
||||
|
||||
char **names = NULL;
|
||||
int n = 0, cap = 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
const char *name = de->d_name;
|
||||
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
|
||||
if (ignored_dir_name(name)) continue;
|
||||
|
||||
char full[MR_PATH_MAX];
|
||||
if (join_path(full, (int)sizeof full, parent, name) != 0) continue;
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0) continue;
|
||||
if (S_ISLNK(st.st_mode)) continue;
|
||||
if (!sibling_is_book(full, &st, archive_pages, ud)) continue;
|
||||
|
||||
if (n >= cap) {
|
||||
int nc = cap ? cap * 2 : 16;
|
||||
char **nn = (char **)realloc(names, (size_t)nc * sizeof *nn);
|
||||
if (!nn) break;
|
||||
names = nn;
|
||||
cap = nc;
|
||||
}
|
||||
size_t len = strlen(name);
|
||||
names[n] = (char *)malloc(len + 1);
|
||||
if (!names[n]) break;
|
||||
memcpy(names[n], name, len + 1);
|
||||
n++;
|
||||
}
|
||||
closedir(d);
|
||||
|
||||
int rc = 0;
|
||||
if (n > 1) qsort(names, (size_t)n, sizeof *names, name_ptr_cmp);
|
||||
int idx = -1;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (strcmp(names[i], cur) == 0) { idx = i; break; }
|
||||
int target = idx + delta;
|
||||
if (idx >= 0 && target >= 0 && target < n) {
|
||||
if (join_path(out, outsz, parent, names[target]) == 0) rc = 1;
|
||||
}
|
||||
for (int i = 0; i < n; i++) free(names[i]);
|
||||
free(names);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int mr_book_adjacent_rooted(const char *dir, int delta, const char *root,
|
||||
mr_lib_archive_fn archive_pages, void *ud,
|
||||
char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return -1;
|
||||
out[0] = '\0';
|
||||
if (!dir || !dir[0] || !root || !root[0]) return -1;
|
||||
/* 只允许在浏览根内翻册:当前册或相邻册越根一律当「没有相邻册」。 */
|
||||
if (!mr_path_within_root(dir, root)) return 0;
|
||||
|
||||
int r = mr_book_adjacent_ex(dir, delta, out, outsz, archive_pages, ud);
|
||||
if (r != 1) return r;
|
||||
if (!mr_path_within_root(out, root)) {
|
||||
out[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz)
|
||||
{
|
||||
return mr_book_adjacent_ex(dir, delta, out, outsz, NULL, NULL);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/* logic/library.h — 书库文件管理器模型(纯 C,不依赖 meow_ui.h)。
|
||||
*
|
||||
* 分层契约同 examples/hello:logic 层只含文件系统与纯数据,宿主 gcc 可直接裸测。
|
||||
*
|
||||
* M18 浏览语义(用户要求「完全像文件管理器」):
|
||||
* - 列表 = 当前目录一层的「子目录 + 受支持图片(png/jpg/jpeg/bmp) + 档案册(.cbz/.zip)」;
|
||||
* 目录优先,组内自然序;其它普通文件不列;隐藏项("." 开头)与符号链接跳过;
|
||||
* - **没有**「目录含图=一本书」的判定:不探测、不算页数、不取封面、不拼续读角标;
|
||||
* 点目录就是进目录(空目录也进),点图片从该图开始读,点档案册打开档案册;
|
||||
* - 扫描只做一次 opendir/readdir + 逐项 lstat;副标题(类型 · 大小)在扫描时生成一次,
|
||||
* 渲染期零 IO、零字符串格式化(UI row 回调直接取指针);
|
||||
* - 单目录条目上限 MR_LIB_SCAN_MAX:到上限后若还有条目则截断并置 lib->truncated=1。
|
||||
*
|
||||
* 仍保留的阅读器辅助:mr_library_dir_has_images / mr_book_adjacent(_ex)
|
||||
* (阅读到册末时翻到同父目录的相邻册,与书库列表模型无关)。
|
||||
*/
|
||||
#ifndef MR_LIBRARY_H
|
||||
#define MR_LIBRARY_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MR_PATH_MAX 512
|
||||
#define MR_NAME_MAX 256
|
||||
#define MR_LIB_MAX_DEPTH 8 /* 兼容旧调用:扫描不递归,该参数被忽略 */
|
||||
|
||||
/* 单目录列出的条目上限(超大目录截断 + UI 提示;可用 -D 覆写便于宿主测试)。 */
|
||||
#ifndef MR_LIB_SCAN_MAX
|
||||
#define MR_LIB_SCAN_MAX 2000
|
||||
#endif
|
||||
|
||||
/* 行类型(列表图标/点击行为由 UI 按此分派)。 */
|
||||
#define MR_LIB_KIND_DIR 0 /* 子目录:点=进入 */
|
||||
#define MR_LIB_KIND_IMAGE 1 /* 受支持图片:点=从该图开始阅读 */
|
||||
#define MR_LIB_KIND_ARCHIVE 2 /* .cbz/.zip 档案册:点=打开档案册 */
|
||||
|
||||
typedef struct mr_library_entry {
|
||||
char name[MR_NAME_MAX]; /* 文件名/目录名(行主标题) */
|
||||
char path[MR_PATH_MAX]; /* 完整路径 */
|
||||
char subtitle[64]; /* 扫描时预生成一次:「文件夹」/「图片 · 1.2 MB」/「档案册 · 1.2 MB」 */
|
||||
long long size; /* 普通文件字节数;目录=0 */
|
||||
int kind; /* MR_LIB_KIND_* */
|
||||
} mr_library_entry;
|
||||
|
||||
typedef struct mr_library {
|
||||
mr_library_entry *items;
|
||||
int count; /* 当前层列出的条目数 */
|
||||
int cap;
|
||||
int truncated; /* 1=达到 MR_LIB_SCAN_MAX 被截断 */
|
||||
} mr_library;
|
||||
|
||||
/* 行数据(纯内存合成;UI 的每帧 src 回调直接取用,零 IO、零格式化)。 */
|
||||
typedef struct mr_library_row {
|
||||
const char *title; /* 主标题 */
|
||||
const char *subtitle; /* 副标题(预生成,永不为 NULL) */
|
||||
int kind; /* MR_LIB_KIND_* */
|
||||
int disabled; /* 1=提示行(不可点,如截断标注) */
|
||||
int index; /* 对应 mr_library.items 下标;提示行=-1 */
|
||||
} mr_library_row;
|
||||
|
||||
/* 扩展名判断:png/jpg/jpeg/bmp(大小写不敏感)。1=受支持图片。 */
|
||||
int mr_path_is_image(const char *name);
|
||||
|
||||
/* 档案册扩展名判断:cbz / zip(大小写不敏感)。1=档案册。 */
|
||||
int mr_path_is_archive(const char *name);
|
||||
|
||||
/* 自然排序比较:数字段按数值比(page2 < page10),数字相等时前导零少的在前;
|
||||
* 非数字段 ASCII 大小写不敏感,完全等价时按原始字节序定序(结果确定,无抖动)。 */
|
||||
int mr_natural_cmp(const char *a, const char *b);
|
||||
|
||||
/* 扫描 dir 的**直接子项**(单层):子目录 + 受支持图片 + 档案册,写入 lib(先释放旧内容)。
|
||||
* 返回条目数(>=0);dir 不可读/不存在/不是目录返回 -1(lib 仍为空且可安全 free)。
|
||||
* max_depth 仅为兼容旧调用保留,已忽略(不再递归)。 */
|
||||
int mr_library_scan(mr_library *lib, const char *dir, int max_depth);
|
||||
|
||||
/* 释放条目数组并清零(可重复调用)。 */
|
||||
void mr_library_free(mr_library *lib);
|
||||
|
||||
/* 取父目录路径(去掉末尾 '/' 后截断最后一段)。根 "/" 返回 "/"。
|
||||
* 成功 0;参数非法/输出缓冲不足 -1。纯路径工具,不做根目录限制(限制在 UI 导航层)。 */
|
||||
int mr_path_parent(const char *path, char *out, int outsz);
|
||||
|
||||
/* 行合成(纯内存、零 IO、O(1);UI 线程每帧调用):idx 越界返回 0。
|
||||
* idx==count 且 lib->truncated 时输出末尾提示行(disabled=1)。
|
||||
* 返回的 title/subtitle 指向 lib 内部或函数内静态缓冲(截断提示行),调用方不要长期持有。 */
|
||||
int mr_library_row_at(const mr_library *lib, int idx, mr_library_row *out);
|
||||
|
||||
/* path 是否落在浏览根内(等于 root 或以 "root/" 开头)。1=在内;0=越界/参数非法。
|
||||
* 根锁定用:书库导航与阅读器「相邻册」都不得越过 SD 卡根。 */
|
||||
int mr_path_within_root(const char *path, const char *root);
|
||||
|
||||
/* 目录是否**直接**含受支持图片。1=有;0=没有/不可读。
|
||||
* 仅用于阅读器的「相邻册」判定,不再是书库行语义。 */
|
||||
int mr_library_dir_has_images(const char *dir);
|
||||
|
||||
/* 册间衔接(M7):同父目录下按自然序排列的「直接含图」兄弟册。
|
||||
* delta=+1 下一册 / -1 上一册。1=找到并写入 out 完整路径;
|
||||
* 0=没有相邻册(当前册是首/末册,或当前册不在兄弟表中);-1=参数非法。
|
||||
* 语义:跳过隐藏/系统目录与符号链接,排序同 mr_natural_cmp。
|
||||
* 只算文件夹册(直接含图目录)——M7 口径不变。 */
|
||||
int mr_book_adjacent(const char *dir, int delta, char *out, int outsz);
|
||||
|
||||
/* 册间衔接 + 档案册(M11):兄弟表 = 「直接含图目录」∪「含图片页的 .cbz/.zip」,
|
||||
* 两者同列按名称自然序混排(跨册衔接按同父目录自然序)。
|
||||
* archive_pages 传 NULL 时与 mr_book_adjacent 完全等价。 */
|
||||
typedef int (*mr_lib_archive_fn)(const char *path, void *ud);
|
||||
int mr_book_adjacent_ex(const char *dir, int delta, char *out, int outsz,
|
||||
mr_lib_archive_fn archive_pages, void *ud);
|
||||
|
||||
/* 册间衔接(根锁定版):在 mr_book_adjacent_ex 之上拒绝浏览根之外的相邻册。
|
||||
* dir 必须在 root 内;找到的相邻册也必须在 root 内,否则按「没有相邻册」返回 0。
|
||||
* 1=找到并写入 out;0=没有相邻册或越根被拒(out 为空串);-1=参数非法。
|
||||
* 阅读器用它保证「读到末页翻册」不会跳到 SD 卡根之外(例如挂载点以上的其它目录)。 */
|
||||
int mr_book_adjacent_rooted(const char *dir, int delta, const char *root,
|
||||
mr_lib_archive_fn archive_pages, void *ud,
|
||||
char *out, int outsz);
|
||||
|
||||
#ifdef MR_LIBRARY_TEST_HOOKS
|
||||
/* 宿主测试用 IO 计数(scan/dir_has_images 的 opendir/readdir/lstat 次数;线程安全不保证)。 */
|
||||
typedef struct mr_library_io_stats {
|
||||
long opendirs;
|
||||
long readdirs;
|
||||
long lstats;
|
||||
} mr_library_io_stats;
|
||||
|
||||
void mr_library_test_io_reset(void);
|
||||
void mr_library_test_io_get(mr_library_io_stats *out);
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MR_LIBRARY_H */
|
||||
@@ -0,0 +1,211 @@
|
||||
/* logic/pin.c — APP 启动 PIN:加盐迭代哈希 + meow_config 持久化(纯 C)。
|
||||
*
|
||||
* 设计取舍(见 pin.h 头注释):PIN 防随手翻看,不是安全边界。本文件不引入加密库,
|
||||
* 只做「每次设置新随机盐 + FNV-1a 迭代 1000 轮」的轻量哈希,避免明文 PIN 直接躺在
|
||||
* 配置里。配置落 TF 数据目录,卡被拔走即可读/改——这是产品口径接受的局限。
|
||||
*
|
||||
* 依赖面:只 include include/meow_ui_config.h(get/set/remove/int 声明),不 include
|
||||
* meow_ui.h、不碰 UI,宿主 gcc 可直接编译 + 测试存根替换配置层。
|
||||
*/
|
||||
#include "pin.h"
|
||||
#include "include/meow_ui_config.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define PIN_KEY_ENABLED "pin.enabled"
|
||||
#define PIN_KEY_HASH "pin.hash"
|
||||
#define PIN_KEY_SALT "pin.salt"
|
||||
|
||||
#define PIN_HEX_LEN 16 /* 64-bit 摘要/盐的 hex 字符数 */
|
||||
#define PIN_HEX_BUF 17 /* 16 + NUL */
|
||||
#define PIN_ROUNDS 1000 /* 迭代轮数(防直接反查,不防暴力枚举) */
|
||||
#define PIN_SALT_BYTES 8 /* 8 字节盐 → 16 位 hex */
|
||||
|
||||
/* ---------- 哈希 ---------- */
|
||||
|
||||
/* FNV-1a(64-bit)逐段吸收,返回新状态。 */
|
||||
static uint64_t fnv1a_update(uint64_t h, const void *data, size_t n)
|
||||
{
|
||||
const unsigned char *p = (const unsigned char *)data;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
h ^= (uint64_t)p[i];
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
/* pin → 16 位 hex 摘要:FNV-1a(salt, pin) 起手,再做 PIN_ROUNDS 轮
|
||||
* 「上一轮摘要(大端 8 字节)+ 盐」的 FNV-1a。同一 PIN 每次设置因盐不同而摘要不同。 */
|
||||
static void pin_digest(const char *pin, const char *salt, char out[PIN_HEX_BUF])
|
||||
{
|
||||
uint64_t h = 1469598103934665603ULL; /* FNV-1a 64-bit offset basis */
|
||||
h = fnv1a_update(h, salt, strlen(salt));
|
||||
h = fnv1a_update(h, pin, strlen(pin));
|
||||
for (int r = 0; r < PIN_ROUNDS; r++) {
|
||||
unsigned char be[8];
|
||||
for (int i = 0; i < 8; i++)
|
||||
be[i] = (unsigned char)((h >> (8 * (7 - i))) & 0xFF);
|
||||
h = fnv1a_update(h, be, sizeof be);
|
||||
h = fnv1a_update(h, salt, strlen(salt));
|
||||
}
|
||||
snprintf(out, PIN_HEX_BUF, "%016llx", (unsigned long long)h);
|
||||
}
|
||||
|
||||
/* 生成 16 位 hex 随机盐:优先 /dev/urandom;无设备时用时间/pid/地址/计数器做降级熵
|
||||
* (宿主测试与裁剪系统仍可用——降级盐的强度如实标注:不构成密码学随机)。 */
|
||||
static void pin_gen_salt(char out[PIN_HEX_BUF])
|
||||
{
|
||||
unsigned char b[PIN_SALT_BYTES];
|
||||
int ok = 0;
|
||||
|
||||
FILE *f = fopen("/dev/urandom", "rb");
|
||||
if (f) {
|
||||
ok = (fread(b, 1, sizeof b, f) == sizeof b);
|
||||
fclose(f);
|
||||
}
|
||||
if (!ok) {
|
||||
static uint64_t ctr;
|
||||
uint64_t x = (uint64_t)time(NULL)
|
||||
^ ((uint64_t)clock() << 21)
|
||||
^ ((uint64_t)getpid() << 40)
|
||||
^ (uint64_t)(uintptr_t)&b
|
||||
^ (++ctr * 0x9E3779B97F4A7C15ULL);
|
||||
for (int i = 0; i < PIN_SALT_BYTES; i++)
|
||||
b[i] = (unsigned char)((x >> (8 * i)) & 0xFF);
|
||||
}
|
||||
|
||||
for (int i = 0; i < PIN_SALT_BYTES; i++)
|
||||
snprintf(out + i * 2, 3, "%02x", b[i]);
|
||||
}
|
||||
|
||||
/* ---------- 配置层小包装 ---------- */
|
||||
static int cfg_get(const char *key, char *out, int outsz)
|
||||
{
|
||||
return meow_config_get(key, out, outsz) == MEOW_CFG_OK ? 0 : -1;
|
||||
}
|
||||
|
||||
static int cfg_set(const char *key, const char *val)
|
||||
{
|
||||
return meow_config_set(key, val) == MEOW_CFG_OK ? 0 : -1;
|
||||
}
|
||||
|
||||
static int is_hex16(const char *s)
|
||||
{
|
||||
if (!s) return 0;
|
||||
for (int i = 0; i < PIN_HEX_LEN; i++) {
|
||||
char c = s[i];
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) return 0;
|
||||
}
|
||||
return s[PIN_HEX_LEN] == '\0';
|
||||
}
|
||||
|
||||
/* ---------- 公共 API ---------- */
|
||||
|
||||
int mr_pin_valid(const char *pin)
|
||||
{
|
||||
if (!pin) return 0;
|
||||
int n = 0;
|
||||
while (pin[n]) {
|
||||
if (pin[n] < '0' || pin[n] > '9') return 0;
|
||||
n++;
|
||||
if (n > MR_PIN_MAX_LEN) return 0;
|
||||
}
|
||||
return n >= MR_PIN_MIN_LEN;
|
||||
}
|
||||
|
||||
int mr_pin_enabled(void)
|
||||
{
|
||||
char hash[PIN_HEX_BUF + 8];
|
||||
char salt[PIN_HEX_BUF + 8];
|
||||
|
||||
if (meow_config_int(PIN_KEY_ENABLED, 0) != 1) return 0;
|
||||
if (cfg_get(PIN_KEY_HASH, hash, (int)sizeof hash) != 0) return 0;
|
||||
if (cfg_get(PIN_KEY_SALT, salt, (int)sizeof salt) != 0) return 0;
|
||||
return is_hex16(hash) && is_hex16(salt); /* 配置残缺 → fail-closed */
|
||||
}
|
||||
|
||||
int mr_pin_set(const char *pin)
|
||||
{
|
||||
char salt[PIN_HEX_BUF];
|
||||
char hash[PIN_HEX_BUF];
|
||||
char old_salt[PIN_HEX_BUF + 8];
|
||||
int had_old_salt;
|
||||
|
||||
if (!mr_pin_valid(pin)) return MR_PIN_ERR_FORMAT;
|
||||
|
||||
pin_gen_salt(salt);
|
||||
pin_digest(pin, salt, hash);
|
||||
had_old_salt = (cfg_get(PIN_KEY_SALT, old_salt, (int)sizeof old_salt) == 0);
|
||||
|
||||
/* 先写盐与摘要、最后打开 enabled:中途失败只会留下「未启用」,不会出现
|
||||
* enabled=1 但缺少盐/哈希的半残状态。摘要写失败时把盐还原成旧值,使已启用的旧
|
||||
* PIN 仍可校验(否则「新盐+旧哈希」会让新旧 PIN 都验不过)。 */
|
||||
if (cfg_set(PIN_KEY_SALT, salt) != 0) return MR_PIN_ERR_STORE;
|
||||
if (cfg_set(PIN_KEY_HASH, hash) != 0) {
|
||||
if (had_old_salt) cfg_set(PIN_KEY_SALT, old_salt);
|
||||
else meow_config_remove(PIN_KEY_SALT);
|
||||
return MR_PIN_ERR_STORE;
|
||||
}
|
||||
if (cfg_set(PIN_KEY_ENABLED, "1") != 0) {
|
||||
/* 重设时 enabled 本来就是 "1":新盐/哈希已一致生效,按成功返回;
|
||||
* 首次开启时 enabled 仍为 0 → 如实报落盘失败。 */
|
||||
return meow_config_int(PIN_KEY_ENABLED, 0) == 1 ? MR_PIN_OK : MR_PIN_ERR_STORE;
|
||||
}
|
||||
return MR_PIN_OK;
|
||||
}
|
||||
|
||||
int mr_pin_verify(const char *pin)
|
||||
{
|
||||
char hash[PIN_HEX_BUF + 8];
|
||||
char salt[PIN_HEX_BUF + 8];
|
||||
char calc[PIN_HEX_BUF];
|
||||
|
||||
if (!mr_pin_valid(pin)) return 0;
|
||||
if (!mr_pin_enabled()) return 0;
|
||||
if (cfg_get(PIN_KEY_HASH, hash, (int)sizeof hash) != 0) return 0;
|
||||
if (cfg_get(PIN_KEY_SALT, salt, (int)sizeof salt) != 0) return 0;
|
||||
|
||||
pin_digest(pin, salt, calc);
|
||||
unsigned diff = 0;
|
||||
for (int i = 0; i < PIN_HEX_LEN; i++)
|
||||
diff |= (unsigned)((unsigned char)hash[i] ^ (unsigned char)calc[i]);
|
||||
diff |= (unsigned)(hash[PIN_HEX_LEN] != '\0');
|
||||
return diff == 0;
|
||||
}
|
||||
|
||||
int mr_pin_change(const char *old_pin, const char *new_pin)
|
||||
{
|
||||
if (!mr_pin_enabled()) return MR_PIN_ERR_STATE;
|
||||
if (!mr_pin_valid(new_pin)) return MR_PIN_ERR_FORMAT;
|
||||
if (!mr_pin_verify(old_pin)) return MR_PIN_ERR_WRONG;
|
||||
return mr_pin_set(new_pin);
|
||||
}
|
||||
|
||||
int mr_pin_disable(void)
|
||||
{
|
||||
meow_config_remove(PIN_KEY_HASH);
|
||||
meow_config_remove(PIN_KEY_SALT);
|
||||
if (cfg_set(PIN_KEY_ENABLED, "0") != 0) return MR_PIN_ERR_STORE;
|
||||
return MR_PIN_OK;
|
||||
}
|
||||
|
||||
int mr_pin_get_salt(char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return 0;
|
||||
out[0] = '\0';
|
||||
if (cfg_get(PIN_KEY_SALT, out, outsz) != 0) return 0;
|
||||
return out[0] != '\0';
|
||||
}
|
||||
|
||||
int mr_pin_get_hash(char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return 0;
|
||||
out[0] = '\0';
|
||||
if (cfg_get(PIN_KEY_HASH, out, outsz) != 0) return 0;
|
||||
return out[0] != '\0';
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/* logic/pin.h — APP 启动 PIN(纯 C;宿主可裸测)。
|
||||
*
|
||||
* 定位(诚实标注):PIN 只挡「随手翻看」——TF 卡可拔出直读,数据目录明文可改,
|
||||
* 故不引入加密库、不宣称安全边界。存储口径:加盐迭代哈希(FNV-1a ×1000 轮,
|
||||
* 每次设置新随机盐,hex 输出),落在 meow_config_*(App 自有 KV → TF MeowData)。
|
||||
* 盐/哈希只做「不知道 PIN 无法直接重算」的轻量门槛,防不了离线暴力或改配置。
|
||||
*
|
||||
* 配置键(meow_config_*,值均为字符串):
|
||||
* pin.enabled = "1"/"0"
|
||||
* pin.hash = 16 位 hex(64-bit 摘要)
|
||||
* pin.salt = 16 位 hex(每次设置重新生成)
|
||||
* enabled=1 但 hash/salt 缺失或格式不合法时一律按「未启用」处理(fail-closed)。
|
||||
*
|
||||
* 本模块只依赖 include/meow_ui_config.h 的三个声明(get/set/remove/int),不碰 UI;
|
||||
* 宿主测试链 test_pin.c 内的 meow_config_* 存根即可,无需 SDK/设备。
|
||||
*/
|
||||
#ifndef MR_PIN_H
|
||||
#define MR_PIN_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MR_PIN_MIN_LEN 4 /* 允许的最短位数 */
|
||||
#define MR_PIN_MAX_LEN 8 /* 允许的最长位数 */
|
||||
|
||||
/* 返回码(set/change/disable)。verify 用 1/0 表示对/错,不返回错误码。 */
|
||||
#define MR_PIN_OK 0 /* 成功 */
|
||||
#define MR_PIN_ERR_FORMAT (-1) /* PIN 非法:非 4-8 位纯数字(NULL/空/符号/超长) */
|
||||
#define MR_PIN_ERR_STATE (-2) /* 状态不满足:change 时尚未启用 */
|
||||
#define MR_PIN_ERR_WRONG (-3) /* change 时原 PIN 校验失败 */
|
||||
#define MR_PIN_ERR_STORE (-4) /* meow_config_* 落盘失败(无数据目录/只读/IO) */
|
||||
|
||||
/* 4-8 位纯 ASCII 数字('0'-'9')→ 1;其余(含 NULL、空串、符号、全角数字)→ 0。 */
|
||||
int mr_pin_valid(const char *pin);
|
||||
|
||||
/* 当前是否启用:1=已启用且 hash/salt 完整合法;0=未启用或配置损坏(fail-closed)。 */
|
||||
int mr_pin_enabled(void);
|
||||
|
||||
/* 设置/重设 PIN(生成新盐 + 迭代哈希并立即落盘)。成功即生效(下次启动进锁屏)。
|
||||
* 返回 MR_PIN_OK / MR_PIN_ERR_FORMAT / MR_PIN_ERR_STORE。 */
|
||||
int mr_pin_set(const char *pin);
|
||||
|
||||
/* 校验:1=匹配;0=不匹配/未启用/输入非法(不区分,避免侧信道式提示)。 */
|
||||
int mr_pin_verify(const char *pin);
|
||||
|
||||
/* 修改:先校验 old,再以新盐重设 new。失败时旧 PIN 保持有效(先校验后写盘)。
|
||||
* 返回 MR_PIN_OK / MR_PIN_ERR_STATE / MR_PIN_ERR_FORMAT / MR_PIN_ERR_WRONG / MR_PIN_ERR_STORE。 */
|
||||
int mr_pin_change(const char *old_pin, const char *new_pin);
|
||||
|
||||
/* 关闭 PIN:删 hash/salt 并把 enabled 置 0(幂等;调用方已通过锁屏验证时无需再输 PIN)。
|
||||
* 返回 MR_PIN_OK / MR_PIN_ERR_STORE。 */
|
||||
int mr_pin_disable(void);
|
||||
|
||||
/* 诊断/测试用:读回当前 hex 盐/哈希。返回 1=读成功,0=未设。 */
|
||||
int mr_pin_get_salt(char *out, int outsz);
|
||||
int mr_pin_get_hash(char *out, int outsz);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MR_PIN_H */
|
||||
@@ -0,0 +1,200 @@
|
||||
/* logic/reader.c — 阅读方向状态(默认 RTL;M4/M6 的阅读器直接复用)+ 每册续读状态(M7)。
|
||||
*
|
||||
* M5 评审 P1 修复:方向设置要「下次启动生效」。最小持久化(不新增任何方向状态):
|
||||
* - mr_reader_set_direction 成功切换后 best-effort 写 meow_config("reader.dir","rtl"/"ltr");
|
||||
* 写失败只打 WARN,不阻断切换(本次运行仍生效);
|
||||
* - mr_reader_get_direction 首次调用惰性读一次配置恢复;无配置/值非法 → 默认 RTL;
|
||||
* 之后走纯内存快路径(不再读配置)。
|
||||
* M7:阅读模式(翻页/条漫)与每册 last page 同为 meow_config 小键(reader.mode/page.<册标识>,
|
||||
* 册标识=规范化目录路径的 FNV-1a64);书库行角标经 mr_cont_badge 读取同一份状态。
|
||||
* M33:mr_cont_resume_page 给「进入目录后的顶栏续读按钮」做可用性判定与页码夹持
|
||||
* (保存页超出当前页数 → 钳到末页;空目录/无图片 → 无入口)。
|
||||
* 依赖面:只 include 配置/日志两个模块头(不含 UI);宿主测试用
|
||||
* test/settings_reader_stub.c 顶替 meow_config_* 与 meow_log。
|
||||
*/
|
||||
#include "reader.h"
|
||||
#include "include/meow_ui_config.h"
|
||||
#include "include/meow_ui_log.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define MR_DIR_CFG_KEY "reader.dir"
|
||||
#define MR_DIR_CFG_LTR "ltr"
|
||||
#define MR_DIR_CFG_RTL "rtl"
|
||||
|
||||
static int g_direction = MR_DIR_RTL;
|
||||
static int g_restored; /* 0=尚未从配置恢复(首次 get/set 时读一次) */
|
||||
|
||||
static void reader_restore_once(void)
|
||||
{
|
||||
char buf[8];
|
||||
|
||||
if (g_restored) return;
|
||||
g_restored = 1; /* 无配置也只读一次:之后纯内存 */
|
||||
if (meow_config_get(MR_DIR_CFG_KEY, buf, sizeof buf) != MEOW_CFG_OK) return;
|
||||
if (strcmp(buf, MR_DIR_CFG_LTR) == 0) g_direction = MR_DIR_LTR;
|
||||
else if (strcmp(buf, MR_DIR_CFG_RTL) == 0) g_direction = MR_DIR_RTL;
|
||||
/* 其它值:忽略,保持默认 RTL */
|
||||
}
|
||||
|
||||
int mr_reader_get_direction(void)
|
||||
{
|
||||
reader_restore_once();
|
||||
return g_direction;
|
||||
}
|
||||
|
||||
void mr_reader_set_direction(int dir)
|
||||
{
|
||||
if (dir != MR_DIR_RTL && dir != MR_DIR_LTR) return;
|
||||
|
||||
reader_restore_once(); /* 先恢复:避免「没 get 过就 set」把配置里的旧方向当默认值 */
|
||||
if (g_direction == dir) return; /* 无变化不写盘 */
|
||||
g_direction = dir;
|
||||
|
||||
if (meow_config_set(MR_DIR_CFG_KEY, dir == MR_DIR_LTR ? MR_DIR_CFG_LTR : MR_DIR_CFG_RTL)
|
||||
!= MEOW_CFG_OK)
|
||||
meow_log(MEOW_LOG_WARN, "reader.dir: save failed (best-effort, kept in memory)");
|
||||
}
|
||||
|
||||
#ifdef MR_READER_TEST_HOOKS
|
||||
void mr_reader_test_reset(void)
|
||||
{
|
||||
g_direction = MR_DIR_RTL;
|
||||
g_restored = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* ============================================================ */
|
||||
/* 阅读模式 + 每册续读状态(M7) */
|
||||
/* ============================================================ */
|
||||
int mr_mode_valid(int mode)
|
||||
{
|
||||
return mode == MR_MODE_PAGED || mode == MR_MODE_WEBTOON;
|
||||
}
|
||||
|
||||
const char *mr_mode_name(int mode)
|
||||
{
|
||||
return mode == MR_MODE_WEBTOON ? "webtoon" : "paged";
|
||||
}
|
||||
|
||||
int mr_mode_parse(const char *s)
|
||||
{
|
||||
if (!s) return -1;
|
||||
if (strcmp(s, "paged") == 0) return MR_MODE_PAGED;
|
||||
if (strcmp(s, "webtoon") == 0) return MR_MODE_WEBTOON;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 目录规范化:去掉末尾 '/'(根 "/" 保留),保证同一册跨调用/跨启动得到同一键。 */
|
||||
static void cont_norm_dir(const char *dir, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return;
|
||||
out[0] = '\0';
|
||||
if (!dir || !dir[0]) return;
|
||||
int n = (int)strlen(dir);
|
||||
while (n > 1 && dir[n - 1] == '/') n--;
|
||||
if (n >= outsz) n = outsz - 1;
|
||||
memcpy(out, dir, (size_t)n);
|
||||
out[n] = '\0';
|
||||
}
|
||||
|
||||
static unsigned long long cont_fnv1a64(const char *s)
|
||||
{
|
||||
unsigned long long h = 1469598103934665603ULL;
|
||||
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
|
||||
h ^= (unsigned long long)*p;
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
int mr_cont_key(const char *dir, const char *field, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return -1;
|
||||
out[0] = '\0';
|
||||
if (!dir || !dir[0] || !field || !field[0]) return -1;
|
||||
char norm[MR_CONT_PATH_MAX];
|
||||
cont_norm_dir(dir, norm, (int)sizeof norm);
|
||||
if (!norm[0]) return -1;
|
||||
int n = snprintf(out, (size_t)outsz, "reader.%s.%016llx", field, cont_fnv1a64(norm));
|
||||
if (n <= 0 || n >= outsz) { out[0] = '\0'; return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_cont_get_page(const char *dir, int def)
|
||||
{
|
||||
char key[64];
|
||||
char buf[24];
|
||||
if (mr_cont_key(dir, "page", key, (int)sizeof key) != 0) return def;
|
||||
if (meow_config_get(key, buf, (int)sizeof buf) != MEOW_CFG_OK) return def;
|
||||
char *end = NULL;
|
||||
long v = strtol(buf, &end, 10);
|
||||
if (end == buf || (end && *end != '\0')) return def;
|
||||
if (v < 0 || v > MR_CONT_PAGE_MAX) return def;
|
||||
return (int)v;
|
||||
}
|
||||
|
||||
void mr_cont_set_page(const char *dir, int page)
|
||||
{
|
||||
char key[64];
|
||||
char val[24];
|
||||
if (page < 0) page = 0;
|
||||
if (page > MR_CONT_PAGE_MAX) page = MR_CONT_PAGE_MAX;
|
||||
if (mr_cont_key(dir, "page", key, (int)sizeof key) != 0) return;
|
||||
snprintf(val, sizeof val, "%d", page);
|
||||
if (meow_config_set(key, val) != MEOW_CFG_OK)
|
||||
meow_log(MEOW_LOG_WARN, "reader: save page failed (best-effort)");
|
||||
}
|
||||
|
||||
int mr_cont_get_mode(const char *dir, int def)
|
||||
{
|
||||
char key[64];
|
||||
char buf[16];
|
||||
if (!mr_mode_valid(def)) def = MR_MODE_PAGED;
|
||||
if (mr_cont_key(dir, "mode", key, (int)sizeof key) != 0) return def;
|
||||
if (meow_config_get(key, buf, (int)sizeof buf) != MEOW_CFG_OK) return def;
|
||||
int m = mr_mode_parse(buf);
|
||||
return m >= 0 ? m : def;
|
||||
}
|
||||
|
||||
void mr_cont_set_mode(const char *dir, int mode)
|
||||
{
|
||||
char key[64];
|
||||
if (!mr_mode_valid(mode)) return;
|
||||
if (mr_cont_key(dir, "mode", key, (int)sizeof key) != 0) return;
|
||||
if (meow_config_set(key, mr_mode_name(mode)) != MEOW_CFG_OK)
|
||||
meow_log(MEOW_LOG_WARN, "reader: save mode failed (best-effort)");
|
||||
}
|
||||
|
||||
void mr_cont_save(const char *dir, int page, int mode)
|
||||
{
|
||||
if (!dir || !dir[0]) return;
|
||||
mr_cont_set_page(dir, page);
|
||||
mr_cont_set_mode(dir, mode);
|
||||
}
|
||||
|
||||
int mr_cont_badge(const char *dir, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return 0;
|
||||
out[0] = '\0';
|
||||
int page = mr_cont_get_page(dir, -1);
|
||||
if (page <= 0) return 0;
|
||||
snprintf(out, (size_t)outsz, "续读 P%d", page + 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_cont_resume_page(const char *dir, int pages)
|
||||
{
|
||||
int page = mr_cont_get_page(dir, -1);
|
||||
if (page < 0) return -1;
|
||||
if (pages == 0) return -1; /* 空目录/无图片:没有可续读的页 */
|
||||
if (pages < 0) return page; /* 页数未知(列表截断):上界交给阅读器钳制 */
|
||||
if (page >= pages) {
|
||||
meow_log(MEOW_LOG_INFO, "reader: resume page %d beyond %d pages (%s), clamp to last",
|
||||
page + 1, pages, dir ? dir : "");
|
||||
return pages - 1;
|
||||
}
|
||||
return page;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/* logic/reader.h — 阅读器纯逻辑接缝(M4 填充实现,M3 先落状态 API;M7 追加模式与每册续读状态)。
|
||||
*
|
||||
* 阅读方向默认 RTL(日式漫画从右往左翻);设置页/阅读器可切 LTR。
|
||||
* 阅读模式(翻页/条漫)与每册 last page 走 meow_config 持久化,键含册标识。
|
||||
* 本模块保持纯 C(不 include meow_ui.h),宿主可直接测。
|
||||
*/
|
||||
#ifndef MR_READER_H
|
||||
#define MR_READER_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MR_DIR_RTL 1 /* 默认:右到左(日漫) */
|
||||
#define MR_DIR_LTR 0 /* 左到右 */
|
||||
|
||||
int mr_reader_get_direction(void);
|
||||
/* 只接受 MR_DIR_RTL / MR_DIR_LTR;其它值忽略(保持原方向)。 */
|
||||
void mr_reader_set_direction(int dir);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 阅读模式(M7):左右翻页 / 条漫竖向连续滚动 */
|
||||
/* ============================================================ */
|
||||
#define MR_MODE_PAGED 0 /* 左右翻页(默认) */
|
||||
#define MR_MODE_WEBTOON 1 /* 条漫:垂直连续滚动 */
|
||||
|
||||
int mr_mode_valid(int mode);
|
||||
const char *mr_mode_name(int mode); /* "paged" / "webtoon"(配置值;非法按 paged) */
|
||||
int mr_mode_parse(const char *s); /* -1=未知 */
|
||||
|
||||
/* ============================================================ */
|
||||
/* 每册续读状态(last page + mode,meow_config 持久化) */
|
||||
/* ------------------------------------------------------------
|
||||
* 键 = reader.<field>.<FNV-1a64(规范化目录路径)>:同一册(同一目录)跨启动恢复,
|
||||
* 不同册互不影响;目录改名/移动视为新册。写失败只 WARN(best-effort,不阻断阅读)。
|
||||
* 依赖:只 include 配置/日志两个模块头,宿主测试用 settings_reader_stub 顶替。
|
||||
* ============================================================ */
|
||||
#define MR_CONT_PATH_MAX 512 /* 册路径规范化的本地缓冲(同 MR_PATH_MAX 口径) */
|
||||
#define MR_CONT_PAGE_MAX 1000000 /* 配置里 last page 的合理上限(防坏值) */
|
||||
|
||||
int mr_cont_key(const char *dir, const char *field, char *out, int outsz);
|
||||
int mr_cont_get_mode(const char *dir, int def);
|
||||
void mr_cont_set_mode(const char *dir, int mode);
|
||||
int mr_cont_get_page(const char *dir, int def);
|
||||
void mr_cont_set_page(const char *dir, int page);
|
||||
/* 一次写 page+mode(尽最大努力,两键分别尝试)。 */
|
||||
void mr_cont_save(const char *dir, int page, int mode);
|
||||
/* 书库行续读角标:1=写入 "续读 P5"(page>0 才有记录);0=无。 */
|
||||
int mr_cont_badge(const char *dir, char *out, int outsz);
|
||||
|
||||
/* 目录内「续读」入口的可用页(M33:按钮在**进入目录后**的顶栏,不在上层列表行)。
|
||||
* pages > 0:该目录当前图片页数;保存页 >= pages(文件被删/目录变短)→ 钳到 pages-1
|
||||
* 并 INFO 日志注明,保证「续读」永远落在有效页;
|
||||
* pages == 0:空目录/无图片 → 恒 -1(入口不显示);
|
||||
* pages < 0:页数未知(书库列表被 MR_LIB_SCAN_MAX 截断)→ 有记录就原样返回,
|
||||
* 上界交给阅读器 open_book 的 mr_nav_clamp_page 钳制。
|
||||
* 返回 -1 = 当前目录没有可用的续读记录(不显示入口)。 */
|
||||
int mr_cont_resume_page(const char *dir, int pages);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MR_READER_H */
|
||||
@@ -0,0 +1,416 @@
|
||||
/* logic/reader/archive_source.c — 档案册(CBZ/zip 当一本书):页源契约的第二种实现。
|
||||
*
|
||||
* 契约、条目过滤与缓存纪律见 page_source.h / page_cache.h。实现要点:
|
||||
* - 页表在 init 时一次建好(mz_open 只读 central directory;只看名字,不解压);
|
||||
* - open(i) 才解压单条目:mz_read_alloc → page_cache 落盘 → 注入的解码回调按路径解码。
|
||||
* 因此翻页/条漫两套消费代码与目录册完全一致(meta 走原始像素尺寸,open 走解码);
|
||||
* - meta(i) 只读该条目的**前 4KB**(mz_read_prefix)取图片头(不落缓存),结果缓存在
|
||||
* meta_w/meta_h;M32 前是整条目解压,500 页排版 = 500 次全量解压;
|
||||
* - 单个 mr_zip_book 的 mz_read* 由自带的互斥量串行化(解码线程与主线程 meta 会碰面)。
|
||||
*
|
||||
* 线程:count/page_name 只读页表,可任意线程;open 可由解码线程调用;meta 按 M6
|
||||
* 用法在主线程调用(条漫排版/当前页几何)。两个 mr_zip_book 实例互不影响,
|
||||
* 解压缓存(page_cache)是进程级共享的。
|
||||
*/
|
||||
#include "logic/reader/page_source.h"
|
||||
#include "logic/reader/page_cache.h"
|
||||
#include "logic/zip/zip.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* ============================================================ */
|
||||
/* 条目过滤:图片后缀 + 路径安全 + 非隐藏 + 非目录 */
|
||||
/* ============================================================ */
|
||||
static int has_control(const char *name, unsigned len)
|
||||
{
|
||||
for (unsigned i = 0; i < len; i++) {
|
||||
unsigned char c = (unsigned char)name[i];
|
||||
if (c < 0x20 || c == 0x7F) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 逐段检查:拒绝 ".." 段与隐藏段("._resource" 之类不参与成册)。 */
|
||||
static int components_safe(const char *name)
|
||||
{
|
||||
const char *p = name;
|
||||
while (*p) {
|
||||
const char *end = strchr(p, '/');
|
||||
size_t n = end ? (size_t)(end - p) : strlen(p);
|
||||
if (n == 2 && p[0] == '.' && p[1] == '.') return 0;
|
||||
if (n > 0 && p[0] == '.') return 0;
|
||||
if (!end) break;
|
||||
p = end + 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_zip_entry_is_page(const char *name, unsigned name_len)
|
||||
{
|
||||
if (!name || name_len == 0 || name_len >= MR_ZIP_MAX_ENTRY_NAME) return 0;
|
||||
if (strlen(name) != name_len) return 0; /* 内嵌 NUL:拒绝 */
|
||||
if (name[0] == '/' || name[0] == '\\') return 0; /* 绝对路径 */
|
||||
if (name_len >= 2 && name[1] == ':') return 0; /* Windows 盘符 */
|
||||
if (strchr(name, '\\')) return 0; /* 反斜杠分隔符:一律拒绝 */
|
||||
if (has_control(name, name_len)) return 0;
|
||||
if (name[name_len - 1] == '/') return 0; /* 目录条目 */
|
||||
if (!components_safe(name)) return 0;
|
||||
return mr_path_is_image(name);
|
||||
}
|
||||
|
||||
/* 页表排序:跨子目录按完整条目名自然序(中文按 UTF-8 字节序,数字段按数值)。 */
|
||||
typedef struct page_ref {
|
||||
const char *name;
|
||||
int eidx;
|
||||
} page_ref;
|
||||
|
||||
static int page_ref_cmp(const void *pa, const void *pb)
|
||||
{
|
||||
const page_ref *a = (const page_ref *)pa;
|
||||
const page_ref *b = (const page_ref *)pb;
|
||||
int c = mr_natural_cmp(a->name, b->name);
|
||||
if (c != 0) return c;
|
||||
return a->eidx < b->eidx ? -1 : (a->eidx > b->eidx ? 1 : 0);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 生命周期 */
|
||||
/* ============================================================ */
|
||||
static int entry_is_page(const mz_entry *e)
|
||||
{
|
||||
if (!e || mz_entry_is_dir(e)) return 0;
|
||||
if (e->flags & 0x0001u) return 0; /* 加密条目:读不了,不列 */
|
||||
if (!mr_zip_entry_is_page(e->name, e->name_len)) return 0;
|
||||
if (e->uncomp_size == 0) return 0;
|
||||
if (e->uncomp_size > (uint64_t)MR_ZIP_PAGE_MAX_BYTES) return 0; /* zip 炸弹 */
|
||||
return 1;
|
||||
}
|
||||
|
||||
void mr_zip_book_free(mr_zip_book *zb)
|
||||
{
|
||||
if (!zb) return;
|
||||
if (zb->arch) {
|
||||
mz_close((mz_archive *)zb->arch);
|
||||
zb->arch = NULL;
|
||||
}
|
||||
if (zb->lock) {
|
||||
pthread_mutex_destroy((pthread_mutex_t *)zb->lock);
|
||||
free(zb->lock);
|
||||
zb->lock = NULL;
|
||||
}
|
||||
free(zb->order);
|
||||
zb->order = NULL;
|
||||
free(zb->meta_w);
|
||||
zb->meta_w = NULL;
|
||||
free(zb->meta_h);
|
||||
zb->meta_h = NULL;
|
||||
zb->count = 0;
|
||||
}
|
||||
|
||||
int mr_zip_book_init(mr_zip_book *zb, const char *path, int maxdim,
|
||||
mr_book_load_fn load, mr_book_unload_fn unload, void *ud)
|
||||
{
|
||||
if (!zb) return -1;
|
||||
memset(zb, 0, sizeof *zb);
|
||||
if (!path || !path[0]) return -1;
|
||||
if (strlen(path) >= sizeof zb->path) return -1;
|
||||
if (maxdim <= 0) maxdim = 720;
|
||||
|
||||
mz_archive *z = NULL;
|
||||
if (mz_open(path, &z) != MZ_OK || !z) return -1;
|
||||
|
||||
pthread_mutex_t *lk = (pthread_mutex_t *)malloc(sizeof *lk);
|
||||
if (!lk) {
|
||||
mz_close(z);
|
||||
return -1;
|
||||
}
|
||||
pthread_mutex_init(lk, NULL);
|
||||
|
||||
snprintf(zb->path, sizeof zb->path, "%s", path);
|
||||
zb->arch = z;
|
||||
zb->lock = lk;
|
||||
zb->maxdim = maxdim;
|
||||
zb->load = load;
|
||||
zb->unload = unload;
|
||||
zb->ud = ud;
|
||||
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0) {
|
||||
zb->mtime = (long long)st.st_mtime;
|
||||
zb->size = (long long)st.st_size;
|
||||
}
|
||||
|
||||
int n = mz_count(z);
|
||||
page_ref *refs = NULL;
|
||||
int nref = 0, cap = 0;
|
||||
for (int i = 0; i < n && nref < MR_BOOK_MAX_PAGES; i++) {
|
||||
const mz_entry *e = mz_entry_at(z, i);
|
||||
if (!entry_is_page(e)) continue;
|
||||
if (nref >= cap) {
|
||||
int nc = cap ? cap * 2 : 32;
|
||||
page_ref *nr = (page_ref *)realloc(refs, (size_t)nc * sizeof *nr);
|
||||
if (!nr) {
|
||||
free(refs);
|
||||
mr_zip_book_free(zb);
|
||||
return -1;
|
||||
}
|
||||
refs = nr;
|
||||
cap = nc;
|
||||
}
|
||||
refs[nref].name = e->name;
|
||||
refs[nref].eidx = i;
|
||||
nref++;
|
||||
}
|
||||
if (nref > 1) qsort(refs, (size_t)nref, sizeof *refs, page_ref_cmp);
|
||||
if (nref > 0) {
|
||||
zb->order = (int *)malloc((size_t)nref * sizeof *zb->order);
|
||||
if (!zb->order) {
|
||||
free(refs);
|
||||
mr_zip_book_free(zb);
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < nref; i++) zb->order[i] = refs[i].eidx;
|
||||
}
|
||||
free(refs);
|
||||
zb->count = nref;
|
||||
return nref;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 页 → 缓存文件 */
|
||||
/* ============================================================ */
|
||||
static const mz_entry *page_entry(mr_zip_book *zb, int index)
|
||||
{
|
||||
if (!zb || !zb->arch || !zb->order) return NULL;
|
||||
if (index < 0 || index >= zb->count) return NULL;
|
||||
return mz_entry_at((mz_archive *)zb->arch, zb->order[index]);
|
||||
}
|
||||
|
||||
int mr_zip_book_page_path(mr_zip_book *zb, int index, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return -1;
|
||||
out[0] = '\0';
|
||||
|
||||
const mz_entry *e = page_entry(zb, index);
|
||||
if (!e) return -1;
|
||||
|
||||
char path[MR_CACHE_FILE_MAX];
|
||||
if (mr_cache_path(zb->path, zb->mtime, zb->size, e->name, index,
|
||||
path, (int)sizeof path) != 0)
|
||||
return -1;
|
||||
if (mr_cache_touch(path)) {
|
||||
snprintf(out, (size_t)outsz, "%s", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
pthread_mutex_lock((pthread_mutex_t *)zb->lock);
|
||||
void *buf = NULL;
|
||||
size_t len = 0;
|
||||
int rc = mz_read_alloc((mz_archive *)zb->arch, e, &buf, &len);
|
||||
pthread_mutex_unlock((pthread_mutex_t *)zb->lock);
|
||||
if (rc != MZ_OK || !buf) {
|
||||
free(buf);
|
||||
return -1;
|
||||
}
|
||||
int ok = mr_cache_store(path, buf, len);
|
||||
free(buf);
|
||||
if (!ok) return -1;
|
||||
snprintf(out, (size_t)outsz, "%s", path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mr_zip_book_page_name(const mr_zip_book *zb, int index, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return;
|
||||
out[0] = '\0';
|
||||
const mz_entry *e = page_entry((mr_zip_book *)zb, index);
|
||||
if (!e) return;
|
||||
snprintf(out, (size_t)outsz, "%s", e->name);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 书库辅助:数页 / 取封面 */
|
||||
/* ============================================================ */
|
||||
int mr_zip_count_pages(const char *path)
|
||||
{
|
||||
if (!path || !path[0]) return 0;
|
||||
mz_archive *z = NULL;
|
||||
if (mz_open(path, &z) != MZ_OK || !z) return 0;
|
||||
int n = mz_count(z), pages = 0;
|
||||
for (int i = 0; i < n && pages < MR_BOOK_MAX_PAGES; i++)
|
||||
if (entry_is_page(mz_entry_at(z, i))) pages++;
|
||||
mz_close(z);
|
||||
return pages;
|
||||
}
|
||||
|
||||
int mr_zip_cover_path(const char *path, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return 0;
|
||||
out[0] = '\0';
|
||||
if (!path || !path[0]) return 0;
|
||||
|
||||
mr_zip_book zb;
|
||||
int n = mr_zip_book_init(&zb, path, 0, NULL, NULL, NULL);
|
||||
int ok = 0;
|
||||
if (n > 0) ok = (mr_zip_book_page_path(&zb, 0, out, outsz) == 0);
|
||||
mr_zip_book_free(&zb);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* mr_book_ops 适配器(档案册) */
|
||||
/* ============================================================ */
|
||||
static int zip_ops_count(void *ctx)
|
||||
{
|
||||
mr_zip_book *zb = (mr_zip_book *)ctx;
|
||||
return zb ? zb->count : 0;
|
||||
}
|
||||
|
||||
/* meta 只读条目**前缀**(≤4KB)取图片头(M32):改前是 mz_read_alloc 整条目解压后再探
|
||||
* (500 页排版 = 500 次全量解压)。PNG IHDR 33 字节、JPEG SOF 在头几百字节、BMP 在
|
||||
* 54 字节 + 调色板内,4KB 足够;前缀探测失败且确实被截断时才回退整条目读(罕见/病态文件)。 */
|
||||
#define MR_ZIP_META_PREFIX 4096
|
||||
|
||||
static int zip_meta_probe_prefix(mr_zip_book *zb, const mz_entry *e, int *w, int *h)
|
||||
{
|
||||
unsigned char head[MR_ZIP_META_PREFIX];
|
||||
size_t head_len = 0;
|
||||
pthread_mutex_lock((pthread_mutex_t *)zb->lock);
|
||||
int rc = mz_read_prefix((mz_archive *)zb->arch, e, head, sizeof head, &head_len);
|
||||
pthread_mutex_unlock((pthread_mutex_t *)zb->lock);
|
||||
if (rc != MZ_OK) return 0;
|
||||
if (mr_probe_size_mem(head, head_len, w, h)) return 1;
|
||||
if (head_len < sizeof head) return 0; /* 整个条目都读到了还探不出 → 坏头 */
|
||||
|
||||
/* 前缀被截断且探不出尺寸(病态长的头部):回退整条目读,保持旧语义。 */
|
||||
void *buf = NULL;
|
||||
size_t len = 0;
|
||||
pthread_mutex_lock((pthread_mutex_t *)zb->lock);
|
||||
rc = mz_read_alloc((mz_archive *)zb->arch, e, &buf, &len);
|
||||
pthread_mutex_unlock((pthread_mutex_t *)zb->lock);
|
||||
if (rc != MZ_OK || !buf) {
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
int known = mr_probe_size_mem(buf, len, w, h);
|
||||
free(buf);
|
||||
return known;
|
||||
}
|
||||
|
||||
/* meta 按需探测图片头;结果缓存,每页只读一次(前缀 4KB,不再整页解压)。主线程调用(M6 用法)。 */
|
||||
static int zip_ops_meta(void *ctx, int index, int *w, int *h)
|
||||
{
|
||||
mr_zip_book *zb = (mr_zip_book *)ctx;
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!zb || index < 0 || index >= zb->count) return 0;
|
||||
|
||||
if (!zb->meta_w) {
|
||||
int *mw = (int *)calloc((size_t)zb->count, sizeof *mw);
|
||||
int *mh = (int *)calloc((size_t)zb->count, sizeof *mh);
|
||||
if (!mw || !mh) {
|
||||
free(mw);
|
||||
free(mh);
|
||||
return 0;
|
||||
}
|
||||
zb->meta_w = mw;
|
||||
zb->meta_h = mh;
|
||||
}
|
||||
if (zb->meta_w[index] > 0 && zb->meta_h[index] > 0) {
|
||||
if (w) *w = zb->meta_w[index];
|
||||
if (h) *h = zb->meta_h[index];
|
||||
return 1;
|
||||
}
|
||||
|
||||
const mz_entry *e = page_entry(zb, index);
|
||||
if (!e) return 0;
|
||||
|
||||
int pw = 0, ph = 0;
|
||||
if (!zip_meta_probe_prefix(zb, e, &pw, &ph)) return 0;
|
||||
|
||||
zb->meta_w[index] = pw;
|
||||
zb->meta_h[index] = ph;
|
||||
if (w) *w = pw;
|
||||
if (h) *h = ph;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static meow_img *zip_ops_open(void *ctx, int index)
|
||||
{
|
||||
mr_zip_book *zb = (mr_zip_book *)ctx;
|
||||
if (!zb || !zb->load) return NULL;
|
||||
char path[MR_CACHE_FILE_MAX];
|
||||
if (mr_zip_book_page_path(zb, index, path, (int)sizeof path) != 0) return NULL;
|
||||
return zb->load(path, zb->ud, zb->maxdim);
|
||||
}
|
||||
|
||||
static void zip_ops_close(void *ctx, meow_img *img)
|
||||
{
|
||||
mr_zip_book *zb = (mr_zip_book *)ctx;
|
||||
if (zb && zb->unload && img) zb->unload(img);
|
||||
}
|
||||
|
||||
static void zip_ops_destroy(void *ctx)
|
||||
{
|
||||
mr_zip_book *zb = (mr_zip_book *)ctx;
|
||||
if (zb) mr_zip_book_free(zb);
|
||||
}
|
||||
|
||||
static const mr_book_ops g_zip_ops = {
|
||||
zip_ops_count, zip_ops_meta, zip_ops_open, zip_ops_close, zip_ops_destroy,
|
||||
};
|
||||
|
||||
void mr_zip_book_bind(mr_book *b, mr_zip_book *zb)
|
||||
{
|
||||
if (!b) return;
|
||||
b->ops = zb ? &g_zip_ops : NULL;
|
||||
b->ctx = zb;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 统一开册 */
|
||||
/* ============================================================ */
|
||||
int mr_book_open_auto(mr_book *b, mr_book_handle *h, const char *path, int maxdim,
|
||||
mr_book_load_fn load, mr_book_unload_fn unload, void *ud)
|
||||
{
|
||||
if (b) {
|
||||
b->ops = NULL;
|
||||
b->ctx = NULL;
|
||||
}
|
||||
if (!b || !h) return -1;
|
||||
memset(h, 0, sizeof *h);
|
||||
if (!path || !path[0]) return -1;
|
||||
|
||||
int n;
|
||||
/* 目录名也可能以 .cbz 结尾(罕见):只有普通文件才当档案册。 */
|
||||
int is_archive = mr_path_is_archive(path);
|
||||
if (is_archive) {
|
||||
struct stat st;
|
||||
if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) is_archive = 0;
|
||||
}
|
||||
if (is_archive) {
|
||||
n = mr_zip_book_init(&h->u.zip, path, maxdim, load, unload, ud);
|
||||
if (n < 0) return -1;
|
||||
h->kind = MR_BOOK_KIND_ZIP;
|
||||
if (n > 0) mr_zip_book_bind(b, &h->u.zip);
|
||||
} else {
|
||||
n = mr_dir_book_init(&h->u.dir, path, maxdim, load, unload, ud);
|
||||
if (n < 0) return -1;
|
||||
h->kind = MR_BOOK_KIND_DIR;
|
||||
if (n > 0) mr_dir_book_bind(b, &h->u.dir);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void mr_book_close_auto(mr_book *b, mr_book_handle *h)
|
||||
{
|
||||
if (b) mr_book_destroy(b);
|
||||
if (!h) return;
|
||||
if (h->kind == MR_BOOK_KIND_ZIP) mr_zip_book_free(&h->u.zip);
|
||||
else if (h->kind == MR_BOOK_KIND_DIR) mr_dir_book_free(&h->u.dir);
|
||||
memset(h, 0, sizeof *h);
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
/* logic/reader/nav.c — 阅读导航纯逻辑实现(见 nav.h 的方向语义说明)。 */
|
||||
#include "logic/reader/nav.h"
|
||||
#include "logic/reader/page_source.h" /* MR_BOOK_MAX_PAGES(条漫排版页数上限) */
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
int mr_nav_tap_zone(int x, int view_w)
|
||||
{
|
||||
if (view_w <= 0) return MR_ZONE_MIDDLE;
|
||||
if (x < view_w / 3) return MR_ZONE_LEFT;
|
||||
if (x >= (view_w * 2) / 3) return MR_ZONE_RIGHT;
|
||||
return MR_ZONE_MIDDLE;
|
||||
}
|
||||
|
||||
int mr_nav_tap_delta(int rtl, int zone)
|
||||
{
|
||||
if (zone == MR_ZONE_MIDDLE) return 0;
|
||||
if (rtl) return (zone == MR_ZONE_LEFT) ? 1 : -1; /* RTL:左进右退 */
|
||||
return (zone == MR_ZONE_LEFT) ? -1 : 1; /* LTR:右进左退 */
|
||||
}
|
||||
|
||||
int mr_nav_swipe_delta(int rtl, int dx)
|
||||
{
|
||||
if (dx == 0) return 0;
|
||||
if (rtl) return dx > 0 ? 1 : -1;
|
||||
return dx > 0 ? -1 : 1;
|
||||
}
|
||||
|
||||
int mr_nav_clamp_page(int page, int count)
|
||||
{
|
||||
if (count <= 0) return -1;
|
||||
if (page < 0) return 0;
|
||||
if (page >= count) return count - 1;
|
||||
return page;
|
||||
}
|
||||
|
||||
int mr_nav_commit_dist(int view_w)
|
||||
{
|
||||
int t = view_w / 4;
|
||||
if (t < 60) t = 60;
|
||||
if (t > 160) t = 160;
|
||||
return t;
|
||||
}
|
||||
|
||||
int mr_nav_commit(int dx, int view_w)
|
||||
{
|
||||
int adx = dx < 0 ? -dx : dx;
|
||||
return adx >= mr_nav_commit_dist(view_w);
|
||||
}
|
||||
|
||||
int mr_nav_fit_size(int fit, int src_w, int src_h, int vw, int vh,
|
||||
int *out_w, int *out_h)
|
||||
{
|
||||
if (!out_w || !out_h || src_w <= 0 || src_h <= 0 || vw <= 0 || vh <= 0) return -1;
|
||||
long long w, h;
|
||||
if (fit == MR_FIT_ORIGINAL) {
|
||||
w = src_w;
|
||||
h = src_h;
|
||||
} else if (fit == MR_FIT_HEIGHT) {
|
||||
h = vh;
|
||||
w = (long long)src_w * vh / src_h;
|
||||
} else {
|
||||
w = vw;
|
||||
h = (long long)src_h * vw / src_w;
|
||||
}
|
||||
if (w < 1) w = 1;
|
||||
if (h < 1) h = 1;
|
||||
*out_w = (int)w;
|
||||
*out_h = (int)h;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_nav_zoom_size(int w, int h, int zoom_pct, int *out_w, int *out_h)
|
||||
{
|
||||
if (!out_w || !out_h || w <= 0 || h <= 0 || zoom_pct <= 0) return -1;
|
||||
long long ww = (long long)w * zoom_pct / 100;
|
||||
long long hh = (long long)h * zoom_pct / 100;
|
||||
if (ww < 1) ww = 1;
|
||||
if (hh < 1) hh = 1;
|
||||
*out_w = (int)ww;
|
||||
*out_h = (int)hh;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mr_nav_pan_limit(int content_w, int content_h, int vw, int vh,
|
||||
int *lim_x, int *lim_y)
|
||||
{
|
||||
int lx = content_w - vw;
|
||||
int ly = content_h - vh;
|
||||
if (lx < 0) lx = 0;
|
||||
if (ly < 0) ly = 0;
|
||||
/* 平移量是「中心偏移」:可左右各移动溢出量的一半,两边才都看得到。 */
|
||||
if (lim_x) *lim_x = lx / 2;
|
||||
if (lim_y) *lim_y = ly / 2;
|
||||
}
|
||||
|
||||
void mr_nav_pan_clamp(int content_w, int content_h, int vw, int vh,
|
||||
int *pan_x, int *pan_y)
|
||||
{
|
||||
int lx, ly;
|
||||
mr_nav_pan_limit(content_w, content_h, vw, vh, &lx, &ly);
|
||||
if (pan_x) {
|
||||
if (*pan_x > lx) *pan_x = lx;
|
||||
if (*pan_x < -lx) *pan_x = -lx;
|
||||
}
|
||||
if (pan_y) {
|
||||
if (*pan_y > ly) *pan_y = ly;
|
||||
if (*pan_y < -ly) *pan_y = -ly;
|
||||
}
|
||||
}
|
||||
|
||||
int mr_nav_slide_from(int rtl, int delta, int view_w)
|
||||
{
|
||||
if (!delta) return 0;
|
||||
int from = rtl ? -delta : delta;
|
||||
return from * view_w;
|
||||
}
|
||||
|
||||
int mr_nav_fit_next(int fit)
|
||||
{
|
||||
if (fit < 0 || fit >= MR_FIT_COUNT) return MR_FIT_WIDTH;
|
||||
return (fit + 1) % MR_FIT_COUNT;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 条漫排版 / 滚动窗口 */
|
||||
/* ============================================================ */
|
||||
static int strip_page_height(int src_w, int src_h, int vw)
|
||||
{
|
||||
long long h;
|
||||
if (vw <= 0) vw = 1;
|
||||
if (src_w > 0 && src_h > 0)
|
||||
h = (long long)src_h * vw / src_w; /* 适宽等比 */
|
||||
else
|
||||
h = (long long)vw * 3 / 2; /* 未知尺寸:2:3 竖图占位 */
|
||||
if (h < 1) h = 1;
|
||||
if (h > MR_STRIP_MAX_PAGE_H) h = MR_STRIP_MAX_PAGE_H;
|
||||
return (int)h;
|
||||
}
|
||||
|
||||
void mr_strip_free(mr_strip *s)
|
||||
{
|
||||
if (!s) return;
|
||||
free(s->top);
|
||||
free(s->height);
|
||||
s->top = NULL;
|
||||
s->height = NULL;
|
||||
s->count = s->total = 0;
|
||||
s->vw = s->vh = 0;
|
||||
}
|
||||
|
||||
int mr_strip_build(mr_strip *s, int count, int vw, int vh,
|
||||
mr_strip_meta_fn meta, void *ctx)
|
||||
{
|
||||
if (!s) return -1;
|
||||
mr_strip_free(s);
|
||||
if (count <= 0 || count > MR_BOOK_MAX_PAGES || vw <= 0 || vh <= 0) return -1;
|
||||
|
||||
s->top = (int *)calloc((size_t)count + 1, sizeof *s->top);
|
||||
s->height = (int *)calloc((size_t)count, sizeof *s->height);
|
||||
if (!s->top || !s->height) { mr_strip_free(s); return -1; }
|
||||
|
||||
s->count = count;
|
||||
s->vw = vw;
|
||||
s->vh = vh;
|
||||
long long y = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int sw = 0, sh = 0;
|
||||
if (meta && !meta(ctx, i, &sw, &sh)) { sw = sh = 0; } /* 未知/失败 → 占位 */
|
||||
int h = strip_page_height(sw, sh, vw);
|
||||
s->height[i] = h;
|
||||
s->top[i] = (int)y;
|
||||
y += h;
|
||||
if (y > 1000000000LL) { /* 累计偏移上限(防溢出;正常书远达不到) */
|
||||
y = 1000000000LL;
|
||||
for (int k = i + 1; k < count; k++) {
|
||||
s->height[k] = 1;
|
||||
s->top[k] = (int)y;
|
||||
y += 1;
|
||||
if (y > 1000000000LL) y = 1000000000LL;
|
||||
}
|
||||
s->top[count] = (int)y;
|
||||
s->total = (int)y;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
s->top[count] = (int)y;
|
||||
s->total = (int)y;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_strip_page_h(const mr_strip *s, int index)
|
||||
{
|
||||
if (!s || !s->height || index < 0 || index >= s->count) return 0;
|
||||
return s->height[index];
|
||||
}
|
||||
|
||||
int mr_strip_top(const mr_strip *s, int index)
|
||||
{
|
||||
if (!s || !s->top || index < 0 || index > s->count) return 0;
|
||||
return s->top[index];
|
||||
}
|
||||
|
||||
int mr_strip_total(const mr_strip *s)
|
||||
{
|
||||
return s ? s->total : 0;
|
||||
}
|
||||
|
||||
int mr_strip_max_offset(const mr_strip *s)
|
||||
{
|
||||
if (!s || s->count <= 0) return 0;
|
||||
int m = s->total - s->vh;
|
||||
return m > 0 ? m : 0;
|
||||
}
|
||||
|
||||
int mr_strip_page_at(const mr_strip *s, int off)
|
||||
{
|
||||
if (!s || s->count <= 0 || !s->top) return -1;
|
||||
if (off <= 0) return 0;
|
||||
if (off >= s->total) return s->count - 1;
|
||||
int lo = 0, hi = s->count - 1;
|
||||
while (lo < hi) { /* 最大的 top[i] <= off */
|
||||
int mid = lo + (hi - lo + 1) / 2;
|
||||
if (s->top[mid] <= off) lo = mid;
|
||||
else hi = mid - 1;
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
|
||||
int mr_strip_offset_for_page(const mr_strip *s, int page)
|
||||
{
|
||||
if (!s || s->count <= 0) return -1;
|
||||
if (page < 0) page = 0;
|
||||
if (page >= s->count) page = s->count - 1;
|
||||
int off = s->top[page];
|
||||
int m = mr_strip_max_offset(s);
|
||||
if (off > m) off = m;
|
||||
if (off < 0) off = 0;
|
||||
return off;
|
||||
}
|
||||
|
||||
int mr_strip_window(const mr_strip *s, int off, int max_pages,
|
||||
int *first, int *last)
|
||||
{
|
||||
if (first) *first = 0;
|
||||
if (last) *last = -1;
|
||||
if (!s || s->count <= 0) return 0;
|
||||
if (max_pages <= 0) max_pages = 1;
|
||||
|
||||
int m = mr_strip_max_offset(s);
|
||||
if (off < 0) off = 0;
|
||||
if (off > m) off = m;
|
||||
|
||||
int f = mr_strip_page_at(s, off);
|
||||
int l = mr_strip_page_at(s, off + s->vh - 1);
|
||||
if (l < f) l = f;
|
||||
|
||||
int lo = f - 1; /* 视口 ±1 页 */
|
||||
int hi = l + 1;
|
||||
if (lo < 0) lo = 0;
|
||||
if (hi > s->count - 1) hi = s->count - 1;
|
||||
if (hi - lo + 1 > max_pages) { /* 驻留上限:从视口中心向外截 */
|
||||
int center = (f + l) / 2;
|
||||
lo = center - (max_pages - 1) / 2;
|
||||
hi = lo + max_pages - 1;
|
||||
if (lo < 0) { lo = 0; hi = max_pages - 1; }
|
||||
if (hi > s->count - 1) { hi = s->count - 1; lo = hi - max_pages + 1; }
|
||||
if (lo < 0) lo = 0;
|
||||
}
|
||||
if (first) *first = lo;
|
||||
if (last) *last = hi;
|
||||
return hi - lo + 1;
|
||||
}
|
||||
|
||||
int mr_strip_at_end(const mr_strip *s, int off)
|
||||
{
|
||||
if (!s || s->count <= 0) return 0;
|
||||
return off >= mr_strip_max_offset(s);
|
||||
}
|
||||
|
||||
int mr_strip_at_start(const mr_strip *s, int off)
|
||||
{
|
||||
if (!s || s->count <= 0) return 0;
|
||||
return off <= 0;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 跳页状态 */
|
||||
/* ============================================================ */
|
||||
void mr_jump_cancel(mr_jump *j)
|
||||
{
|
||||
if (!j) return;
|
||||
j->pending = 0;
|
||||
j->target = -1;
|
||||
j->t0 = 0;
|
||||
}
|
||||
|
||||
void mr_jump_begin(mr_jump *j, int page, int count, unsigned long long now)
|
||||
{
|
||||
if (!j) return;
|
||||
if (count <= 0 || page < 0 || page >= count) { mr_jump_cancel(j); return; }
|
||||
j->pending = 1;
|
||||
j->target = page;
|
||||
j->t0 = now;
|
||||
}
|
||||
|
||||
int mr_jump_active(const mr_jump *j)
|
||||
{
|
||||
return j && j->pending ? 1 : 0;
|
||||
}
|
||||
|
||||
int mr_jump_target(const mr_jump *j)
|
||||
{
|
||||
return (j && j->pending) ? j->target : -1;
|
||||
}
|
||||
|
||||
int mr_jump_poll(mr_jump *j, int have_img, unsigned long long now,
|
||||
unsigned long long timeout_ms, int *timed_out)
|
||||
{
|
||||
if (timed_out) *timed_out = 0;
|
||||
if (!j || !j->pending) return 0;
|
||||
if (have_img) { mr_jump_cancel(j); return 1; }
|
||||
if (timeout_ms > 0 && now - j->t0 >= timeout_ms) {
|
||||
mr_jump_cancel(j);
|
||||
if (timed_out) *timed_out = 1;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/* logic/reader/nav.h — 阅读导航纯逻辑(方向语义 / tap 分区 / fit / 平移 clamp)。
|
||||
*
|
||||
* 纯 C、无 SDK 依赖:reader.c 与宿主测试共用同一实现(logic/reader/nav.c)。
|
||||
*
|
||||
* 方向语义(mr_reader_get_direction,默认 RTL):
|
||||
* - LTR(左→右读):页码沿屏幕向右递增;屏左 1/3 = 上一页,屏右 1/3 = 下一页;
|
||||
* 手指向右拖露出左边那页 = 上一页。
|
||||
* - RTL(日漫,默认):页码沿屏幕向左递增;屏左 1/3 = **下一页**,屏右 1/3 = 上一页;
|
||||
* 手指向右拖露出左边那页 = 下一页。
|
||||
* 图像本身不做镜像(RTL 只影响翻页语义)。
|
||||
*/
|
||||
#ifndef MR_READER_NAV_H
|
||||
#define MR_READER_NAV_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- fit 三档 ---- */
|
||||
#define MR_FIT_WIDTH 0 /* 适宽:撑满屏宽 */
|
||||
#define MR_FIT_HEIGHT 1 /* 适高:撑满屏高 */
|
||||
#define MR_FIT_ORIGINAL 2 /* 原始:1:1 像素 */
|
||||
#define MR_FIT_COUNT 3
|
||||
|
||||
/* ---- tap 分区(按屏幕横向三分) ---- */
|
||||
#define MR_ZONE_LEFT 0
|
||||
#define MR_ZONE_MIDDLE 1
|
||||
#define MR_ZONE_RIGHT 2
|
||||
|
||||
/* 屏宽三分:左/中/右。view_w<=0 返回 MIDDLE。 */
|
||||
int mr_nav_tap_zone(int x, int view_w);
|
||||
|
||||
/* tap 分区的页增量:+1=下一页 / -1=上一页 / 0=chrome 显隐(中区)。 */
|
||||
int mr_nav_tap_delta(int rtl, int zone);
|
||||
|
||||
/* 中部横向拖动的页增量。dx>0 = 手指向右(露出屏幕左侧那页)。
|
||||
* 首/末页不在这里判断——调用方用 mr_nav_clamp_page 落到实际目标页。 */
|
||||
int mr_nav_swipe_delta(int rtl, int dx);
|
||||
|
||||
/* 页下标 clamp;count<=0 返回 -1。 */
|
||||
int mr_nav_clamp_page(int page, int count);
|
||||
|
||||
/* 翻页提交阈值:视口宽的 1/4,夹在 [60,160] px。 */
|
||||
int mr_nav_commit_dist(int view_w);
|
||||
|
||||
/* 松手位移是否达到翻页阈值。 */
|
||||
int mr_nav_commit(int dx, int view_w);
|
||||
|
||||
/* fit 目标尺寸(未含 zoom)。0=成功;参数非法 -1。 */
|
||||
int mr_nav_fit_size(int fit, int src_w, int src_h, int vw, int vh, int *out_w, int *out_h);
|
||||
|
||||
/* 按百分比放大(zoom_pct=100 原样;200=2×)。0=成功。 */
|
||||
int mr_nav_zoom_size(int w, int h, int zoom_pct, int *out_w, int *out_h);
|
||||
|
||||
/* 平移半幅:内容比视口每侧多出的像素(>=0)。内容不溢出该轴 → 0(该轴不可平移)。 */
|
||||
void mr_nav_pan_limit(int content_w, int content_h, int vw, int vh, int *lim_x, int *lim_y);
|
||||
|
||||
/* 把平移量夹到 ±半幅。 */
|
||||
void mr_nav_pan_clamp(int content_w, int content_h, int vw, int vh, int *pan_x, int *pan_y);
|
||||
|
||||
/* 翻页动画的进场起点(px):下一页在 RTL 从左侧进、LTR 从右侧进。delta=0 返回 0。 */
|
||||
int mr_nav_slide_from(int rtl, int delta, int view_w);
|
||||
|
||||
/* fit 档循环:适宽 → 适高 → 原始 → 适宽。 */
|
||||
int mr_nav_fit_next(int fit);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 条漫(M7):竖向连续滚动的排版 / 滚动窗口 / 跳页状态 */
|
||||
/* ------------------------------------------------------------
|
||||
* 排版用页源契约的 meta()(原始像素尺寸,不解码):每页按屏宽等比换算显示高度,
|
||||
* 累加成「整条」的 y 偏移表。未知尺寸的页按 2:3 竖图占位(排版稳定,解码后不改布局)。
|
||||
* 全部纯整数运算、无 SDK 依赖;reader.c 与宿主测试共用(logic/reader/nav.c)。
|
||||
* ============================================================ */
|
||||
#define MR_STRIP_MAX_PAGE_H 200000 /* 单页显示高度上限(防坏头/极端长图撑爆累计偏移) */
|
||||
|
||||
typedef struct mr_strip {
|
||||
int count;
|
||||
int vw, vh;
|
||||
int *top; /* count+1 个:top[i]=第 i 页顶端偏移;top[count]=整条总高 */
|
||||
int *height; /* count 个:每页显示高度(>=1) */
|
||||
int total;
|
||||
} mr_strip;
|
||||
|
||||
/* 页尺寸回调(通常是 mr_book_meta):1=已知原始尺寸,0=未知。 */
|
||||
typedef int (*mr_strip_meta_fn)(void *ctx, int index, int *w, int *h);
|
||||
|
||||
/* 建排版表。0=成功;参数非法/内存不足 -1(已清空、可安全 free)。 */
|
||||
int mr_strip_build(mr_strip *s, int count, int vw, int vh,
|
||||
mr_strip_meta_fn meta, void *ctx);
|
||||
/* 释放偏移表并清零(可重复调用)。 */
|
||||
void mr_strip_free(mr_strip *s);
|
||||
|
||||
int mr_strip_page_h(const mr_strip *s, int index); /* 越界 0 */
|
||||
int mr_strip_top(const mr_strip *s, int index); /* index==count 返回总高;越界 0 */
|
||||
int mr_strip_total(const mr_strip *s);
|
||||
int mr_strip_max_offset(const mr_strip *s); /* max(0, total-vh) */
|
||||
/* 视口顶端(off)落在哪一页;空册 -1。 */
|
||||
int mr_strip_page_at(const mr_strip *s, int off);
|
||||
/* 落位到某页顶端所需的滚动量(夹到 [0,max_offset]);空册 -1。 */
|
||||
int mr_strip_offset_for_page(const mr_strip *s, int page);
|
||||
/* 视口可见页范围 [first,last] + ±1 解码窗,条数不超过 max_pages。
|
||||
* 返回窗口页数;空册 0。first/last 可 NULL。 */
|
||||
int mr_strip_window(const mr_strip *s, int off, int max_pages,
|
||||
int *first, int *last);
|
||||
int mr_strip_at_end(const mr_strip *s, int off); /* 1=已到条底 */
|
||||
int mr_strip_at_start(const mr_strip *s, int off); /* 1=已在条顶 */
|
||||
|
||||
/* ---- 跳页(chrome 滑块):目标页预解码完成后再落位,避免白屏/闪烁 ---- */
|
||||
typedef struct mr_jump {
|
||||
int pending; /* 1=正在等目标页 */
|
||||
int target; /* 目标页;pending==0 时无意义 */
|
||||
unsigned long long t0; /* 开始等待的时刻(ms) */
|
||||
} mr_jump;
|
||||
|
||||
/* 开始跳页:page 夹到 [0,count);count<=0 或 page<0 时取消。 */
|
||||
void mr_jump_begin(mr_jump *j, int page, int count, unsigned long long now);
|
||||
int mr_jump_active(const mr_jump *j);
|
||||
int mr_jump_target(const mr_jump *j); /* 无 pending 返回 -1 */
|
||||
void mr_jump_cancel(mr_jump *j);
|
||||
/* 轮询:have_img=目标页是否已解码。1=可以落位(已清除 pending);
|
||||
* timed_out 非 NULL 时写入「是否因超时落位」(调用方据此提示降级)。
|
||||
* 未就绪且未超时返回 0(继续等)。 */
|
||||
int mr_jump_poll(mr_jump *j, int have_img, unsigned long long now,
|
||||
unsigned long long timeout_ms, int *timed_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MR_READER_NAV_H */
|
||||
@@ -0,0 +1,366 @@
|
||||
/* logic/reader/page_cache.c — 解压页磁盘缓存实现,见 page_cache.h 的纪律说明。 */
|
||||
#include "logic/reader/page_cache.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef struct cache_entry {
|
||||
char path[MR_CACHE_FILE_MAX];
|
||||
long long size;
|
||||
unsigned long long used; /* LRU 计数(越大越新) */
|
||||
} cache_entry;
|
||||
|
||||
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static char g_dir[MR_CACHE_DIR_MAX];
|
||||
static int g_ready;
|
||||
static cache_entry g_idx[MR_CACHE_MAX_PAGES];
|
||||
static int g_n;
|
||||
static long long g_bytes;
|
||||
static unsigned long long g_tick;
|
||||
|
||||
/* ============================================================ */
|
||||
/* 目录推导 */
|
||||
/* ============================================================ */
|
||||
int mr_cache_dir_from_data(const char *data_dir, char *out, int outsz)
|
||||
{
|
||||
if (!data_dir || !data_dir[0] || !out || outsz <= 0) return -1;
|
||||
|
||||
size_t n = strlen(data_dir);
|
||||
while (n > 1 && data_dir[n - 1] == '/') n--;
|
||||
if (n >= (size_t)outsz) return -1;
|
||||
memcpy(out, data_dir, n);
|
||||
out[n] = '\0';
|
||||
|
||||
/* 末段是 "data" 时换成 "cache"(数据目录 → 同级缓存目录)。 */
|
||||
char *slash = strrchr(out, '/');
|
||||
if (slash && strcmp(slash + 1, "data") == 0) *slash = '\0';
|
||||
|
||||
n = strlen(out);
|
||||
if (n == 0) {
|
||||
if (outsz < 7) return -1;
|
||||
memcpy(out, "/cache", 7);
|
||||
return 0;
|
||||
}
|
||||
int w = snprintf(out + n, (size_t)(outsz - (int)n), "/cache");
|
||||
if (w <= 0 || w >= outsz - (int)n) {
|
||||
out[0] = '\0';
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_cache_is_page_name(const char *name)
|
||||
{
|
||||
return name && strncmp(name, MR_CACHE_PREFIX, strlen(MR_CACHE_PREFIX)) == 0;
|
||||
}
|
||||
|
||||
const char *mr_cache_dir(void)
|
||||
{
|
||||
return g_ready ? g_dir : "";
|
||||
}
|
||||
|
||||
int mr_cache_ready(void)
|
||||
{
|
||||
return g_ready;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 索引(调用方持锁) */
|
||||
/* ============================================================ */
|
||||
static int idx_find_locked(const char *path)
|
||||
{
|
||||
for (int i = 0; i < g_n; i++)
|
||||
if (strcmp(g_idx[i].path, path) == 0) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void idx_remove_locked(int i)
|
||||
{
|
||||
if (i < 0 || i >= g_n) return;
|
||||
g_bytes -= g_idx[i].size;
|
||||
g_idx[i] = g_idx[g_n - 1];
|
||||
g_n--;
|
||||
}
|
||||
|
||||
static void idx_evict_locked(void)
|
||||
{
|
||||
if (g_n <= 0) return;
|
||||
int oldest = 0;
|
||||
for (int i = 1; i < g_n; i++)
|
||||
if (g_idx[i].used < g_idx[oldest].used) oldest = i;
|
||||
unlink(g_idx[oldest].path);
|
||||
idx_remove_locked(oldest);
|
||||
}
|
||||
|
||||
/* 腾出 len 字节:页数或字节先到上限就淘汰最旧。单条 len 本身超过
|
||||
* MR_CACHE_MAX_BYTES 时会清空其它条目、让这一条独占(文档化的一次性溢出,
|
||||
* 上限由页源单页 64MiB 约束)——大页仍可读,不因预算拒绝落缓存。 */
|
||||
static void make_room_locked(long long len)
|
||||
{
|
||||
while (g_n > 0 && (g_n >= MR_CACHE_MAX_PAGES ||
|
||||
g_bytes + len > MR_CACHE_MAX_BYTES))
|
||||
idx_evict_locked();
|
||||
}
|
||||
|
||||
static void idx_add_locked(const char *path, long long size)
|
||||
{
|
||||
if (g_n >= MR_CACHE_MAX_PAGES) return;
|
||||
cache_entry *e = &g_idx[g_n++];
|
||||
snprintf(e->path, sizeof e->path, "%s", path);
|
||||
e->size = size;
|
||||
g_tick++;
|
||||
e->used = g_tick;
|
||||
g_bytes += size;
|
||||
}
|
||||
|
||||
/* 上一进程遗留的解压页:索引在内存里,磁盘上的 mrpage_* 全是孤儿。 */
|
||||
static void clean_orphans_locked(void)
|
||||
{
|
||||
DIR *d = opendir(g_dir);
|
||||
if (!d) return;
|
||||
size_t plen = strlen(MR_CACHE_PREFIX);
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strncmp(de->d_name, MR_CACHE_PREFIX, plen) != 0) continue;
|
||||
char full[MR_CACHE_FILE_MAX];
|
||||
int w = snprintf(full, sizeof full, "%s/%s", g_dir, de->d_name);
|
||||
if (w > 0 && w < (int)sizeof full) unlink(full);
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 生命周期 */
|
||||
/* ============================================================ */
|
||||
/* 逐级建目录(数据目录的父级可能还不存在;只建到缓存目录为止)。 */
|
||||
static int mkdir_p(const char *path)
|
||||
{
|
||||
char tmp[MR_CACHE_DIR_MAX];
|
||||
size_t n = strlen(path);
|
||||
if (n == 0 || n >= sizeof tmp) return -1;
|
||||
memcpy(tmp, path, n + 1);
|
||||
for (size_t i = 1; i <= n; i++) {
|
||||
if (tmp[i] != '/' && tmp[i] != '\0') continue;
|
||||
char ch = tmp[i];
|
||||
tmp[i] = '\0';
|
||||
if (mkdir(tmp, 0755) != 0 && errno != EEXIST) {
|
||||
struct stat st;
|
||||
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
|
||||
}
|
||||
tmp[i] = ch;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_cache_init(const char *data_dir)
|
||||
{
|
||||
char dir[MR_CACHE_DIR_MAX];
|
||||
if (mr_cache_dir_from_data(data_dir, dir, (int)sizeof dir) != 0) return -1;
|
||||
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (g_ready && strcmp(g_dir, dir) == 0) {
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_ready = 0;
|
||||
g_n = 0;
|
||||
g_bytes = 0;
|
||||
g_dir[0] = '\0';
|
||||
|
||||
if (mkdir_p(dir) != 0) {
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return -1;
|
||||
}
|
||||
struct stat st;
|
||||
if (stat(dir, &st) != 0 || !S_ISDIR(st.st_mode)) {
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return -1;
|
||||
}
|
||||
snprintf(g_dir, sizeof g_dir, "%s", dir);
|
||||
clean_orphans_locked();
|
||||
g_tick = 0;
|
||||
g_ready = 1;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void mr_cache_shutdown(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_lock);
|
||||
g_ready = 0;
|
||||
g_n = 0;
|
||||
g_bytes = 0;
|
||||
g_tick = 0;
|
||||
g_dir[0] = '\0';
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 键 / 命中 / 写入 */
|
||||
/* ============================================================ */
|
||||
static unsigned long long hash_bytes(unsigned long long h, const void *p, size_t n)
|
||||
{
|
||||
const unsigned char *b = (const unsigned char *)p;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
h ^= (unsigned long long)b[i];
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
static unsigned long long cache_hash(const char *archive, long long mtime,
|
||||
long long size, const char *entry, int page)
|
||||
{
|
||||
char num[64];
|
||||
unsigned long long h = 1469598103934665603ULL;
|
||||
h = hash_bytes(h, archive, strlen(archive) + 1);
|
||||
int n = snprintf(num, sizeof num, "%lld", mtime);
|
||||
h = hash_bytes(h, num, (size_t)n + 1);
|
||||
n = snprintf(num, sizeof num, "%lld", size);
|
||||
h = hash_bytes(h, num, (size_t)n + 1);
|
||||
h = hash_bytes(h, entry, strlen(entry) + 1);
|
||||
n = snprintf(num, sizeof num, "%d", page);
|
||||
h = hash_bytes(h, num, (size_t)n + 1);
|
||||
return h;
|
||||
}
|
||||
|
||||
/* 保留图片扩展名:解码入口按 .jpg/.jpeg 区分「解码期缩解」路径。 */
|
||||
static void entry_ext(const char *entry, char *out, int outsz)
|
||||
{
|
||||
const char *dot = entry ? strrchr(entry, '.') : NULL;
|
||||
const char *ext = dot ? dot + 1 : "";
|
||||
if ((strcasecmp(ext, "jpg") == 0) || (strcasecmp(ext, "jpeg") == 0) ||
|
||||
(strcasecmp(ext, "png") == 0) || (strcasecmp(ext, "bmp") == 0)) {
|
||||
int i = 0;
|
||||
for (; ext[i] && i < outsz - 1; i++) out[i] = (char)tolower((unsigned char)ext[i]);
|
||||
out[i] = '\0';
|
||||
return;
|
||||
}
|
||||
snprintf(out, (size_t)outsz, "img");
|
||||
}
|
||||
|
||||
int mr_cache_path(const char *archive, long long mtime, long long size,
|
||||
const char *entry, int page_index, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return -1;
|
||||
out[0] = '\0';
|
||||
if (!archive || !archive[0] || !entry || !entry[0]) return -1;
|
||||
|
||||
char ext[8];
|
||||
entry_ext(entry, ext, (int)sizeof ext);
|
||||
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (!g_ready) {
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return -1;
|
||||
}
|
||||
unsigned long long h = cache_hash(archive, mtime, size, entry, page_index);
|
||||
int n = snprintf(out, (size_t)outsz, "%s/%s%016llx.%s", g_dir, MR_CACHE_PREFIX, h, ext);
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
if (n <= 0 || n >= outsz) {
|
||||
out[0] = '\0';
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_cache_touch(const char *path)
|
||||
{
|
||||
if (!path || !path[0]) return 0;
|
||||
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (!g_ready) {
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return 0;
|
||||
}
|
||||
int i = idx_find_locked(path);
|
||||
/* 命中也要 stat 校验(P2-1 纵深防御):外部清理、掉电残留、或别的 LRU 误删
|
||||
* 都会让索引指向不存在的文件;假命中会让页源返回死路径且永不自愈。缺失或
|
||||
* 尺寸不符就从索引移除、按未命中处理,让调用方重新解压。 */
|
||||
struct stat st;
|
||||
int exists = stat(path, &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0;
|
||||
if (i >= 0) {
|
||||
if (!exists || (long long)st.st_size != g_idx[i].size) {
|
||||
idx_remove_locked(i);
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return 0;
|
||||
}
|
||||
g_tick++;
|
||||
g_idx[i].used = g_tick;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return 1;
|
||||
}
|
||||
/* 索引里没有(外部写入/索引满):磁盘上有就算命中,能装进索引就装。 */
|
||||
if (exists && g_n < MR_CACHE_MAX_PAGES && strlen(path) < MR_CACHE_FILE_MAX)
|
||||
idx_add_locked(path, (long long)st.st_size);
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return exists ? 1 : 0;
|
||||
}
|
||||
|
||||
int mr_cache_store(const char *path, const void *data, size_t len)
|
||||
{
|
||||
if (!path || !path[0] || !data || len == 0) return 0;
|
||||
if (strlen(path) >= MR_CACHE_FILE_MAX) return 0;
|
||||
|
||||
char tmp[MR_CACHE_FILE_MAX + 8];
|
||||
int w = snprintf(tmp, sizeof tmp, "%s.part", path);
|
||||
if (w <= 0 || w >= (int)sizeof tmp) return 0;
|
||||
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (!g_ready) {
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return 0;
|
||||
}
|
||||
int i = idx_find_locked(path);
|
||||
if (i >= 0) idx_remove_locked(i);
|
||||
make_room_locked((long long)len);
|
||||
pthread_mutex_unlock(&g_lock); /* 写盘不持锁:大页不阻塞并发读者 */
|
||||
|
||||
FILE *f = fopen(tmp, "wb");
|
||||
int ok = f && fwrite(data, 1, len, f) == len;
|
||||
if (f && fclose(f) != 0) ok = 0;
|
||||
if (!ok) {
|
||||
unlink(tmp);
|
||||
return 0;
|
||||
}
|
||||
if (rename(tmp, path) != 0) {
|
||||
unlink(tmp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_lock);
|
||||
if (g_ready) {
|
||||
i = idx_find_locked(path);
|
||||
if (i >= 0) idx_remove_locked(i);
|
||||
make_room_locked((long long)len); /* 写盘期间可能被别的线程填满 */
|
||||
idx_add_locked(path, (long long)len);
|
||||
g_tick++;
|
||||
}
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_cache_count(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_lock);
|
||||
int n = g_n;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return n;
|
||||
}
|
||||
|
||||
long long mr_cache_bytes(void)
|
||||
{
|
||||
pthread_mutex_lock(&g_lock);
|
||||
long long b = g_bytes;
|
||||
pthread_mutex_unlock(&g_lock);
|
||||
return b;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/* logic/reader/page_cache.h — 档案册解压页的磁盘缓存(纯 C,不依赖 SDK)。
|
||||
*
|
||||
* 为什么需要磁盘缓存:SDK 的图像解码入口只吃文件路径(meow_img_load_file_dim),
|
||||
* 而 zip 里的页是压缩流。档案页源在 open(i) 时把条目解压成缓存文件,再用同一个
|
||||
* 解码回调按路径解码——两种阅读模式(翻页/条漫)与目录册走完全相同的代码路径。
|
||||
*
|
||||
* 目录与上限(纪律):
|
||||
* - 目录 = <数据目录>/cache(数据目录形如 .../MeowData/os.meow.picbrowser/data);
|
||||
* 文件名统一前缀 mrpage_,与其它可能落在这目录的临时文件互不影响;
|
||||
* - 上限 = 64 页 或 32MiB,先到者触发 LRU 淘汰(按最近使用计数);
|
||||
* - **单条目溢出**:某页解压后本身就超过 32MiB 时允许一次性溢出——只保留该条、
|
||||
* 其它条目全部淘汰,占用上限受页源单页上限 64MiB(MR_ZIP_PAGE_MAX_BYTES)约束。
|
||||
* 这样超大页仍可阅读,不会因为预算拒绝落缓存而变成永久空白页;
|
||||
* - mr_cache_init() 首次成功时清掉上一进程遗留的 mrpage_*(孤儿页);
|
||||
* - 写入走 "<name>.part" + rename,进程崩溃不会留下半个缓存文件。
|
||||
*
|
||||
* 键 = 档案路径 + 档案 mtime/size + 条目名 + 页下标:档案被替换(mtime/size 变)或
|
||||
* 同名条目重复出现时不会命中旧内容。文件名保留图片扩展名(解码入口按扩展名区分
|
||||
* JPEG 的「解码期缩解」路径)。
|
||||
*
|
||||
* 线程:内部互斥量;可在解码线程/主线程并发调用。
|
||||
* 纯 C:宿主测试可直接编译(见 tuku/test/test_cbz.c)。
|
||||
*/
|
||||
#ifndef MR_PAGE_CACHE_H
|
||||
#define MR_PAGE_CACHE_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MR_CACHE_MAX_PAGES 64
|
||||
#define MR_CACHE_MAX_BYTES (32LL * 1024 * 1024)
|
||||
#define MR_CACHE_PREFIX "mrpage_"
|
||||
#define MR_CACHE_DIR_MAX 200
|
||||
#define MR_CACHE_FILE_MAX 256
|
||||
|
||||
/* 数据目录 → 缓存目录(纯字符串):末尾的 "data" 段替换为 "cache"。
|
||||
* 例:/…/MeowData/os.meow.picbrowser/data → /…/MeowData/os.meow.picbrowser/cache。
|
||||
* 成功 0;参数非法/缓冲不足 -1。 */
|
||||
int mr_cache_dir_from_data(const char *data_dir, char *out, int outsz);
|
||||
|
||||
/* 文件名是否属于解压页缓存(mrpage_ 前缀)。启动清孤儿用这一判据,
|
||||
* 保证同目录的其它文件不被误删。1=解压页缓存。 */
|
||||
int mr_cache_is_page_name(const char *name);
|
||||
|
||||
/* 建立/切换缓存目录并清孤儿。0=可用;-1=不可用(之后 store/touch 均失败)。
|
||||
* 同一目录重复调用为无操作(不会删掉正在用的页)。 */
|
||||
int mr_cache_init(const char *data_dir);
|
||||
|
||||
/* 释放索引(不删文件);测试用。可重复调用。 */
|
||||
void mr_cache_shutdown(void);
|
||||
|
||||
int mr_cache_ready(void);
|
||||
const char *mr_cache_dir(void); /* 未初始化返回空串 */
|
||||
|
||||
/* 计算某页缓存文件路径(不碰磁盘)。扩展名取自 entry 名(未知时 .img)。 */
|
||||
int mr_cache_path(const char *archive, long long mtime, long long size,
|
||||
const char *entry, int page_index, char *out, int outsz);
|
||||
|
||||
/* 缓存命中:文件在索引里(或磁盘上存在)则刷新 LRU 并返回 1,否则 0。 */
|
||||
int mr_cache_touch(const char *path);
|
||||
|
||||
/* 写入缓存并纳入 LRU;1=成功。len==0 视为失败(空页没有解码价值)。 */
|
||||
int mr_cache_store(const char *path, const void *data, size_t len);
|
||||
|
||||
/* 观测(宿主测试断言 LRU 纪律用):当前索引页数 / 字节数。 */
|
||||
int mr_cache_count(void);
|
||||
long long mr_cache_bytes(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MR_PAGE_CACHE_H */
|
||||
@@ -0,0 +1,747 @@
|
||||
/* logic/reader/page_source.c — 页源契约实现(纯 C):目录册 + 图片头探测。
|
||||
*
|
||||
* 契约与两种消费方式的说明见 page_source.h。本文件不含 SDK 依赖:解码/释放由
|
||||
* UI 层注回回调,因此可在宿主测试里用假回调覆盖「空册/越界/重复 open-close/首末页」。
|
||||
*/
|
||||
#include "logic/reader/page_source.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* ============================================================ */
|
||||
/* 契约包装 */
|
||||
/* ============================================================ */
|
||||
int mr_book_valid(const mr_book *b)
|
||||
{
|
||||
return b && b->ops && b->ops->count && b->ops->open;
|
||||
}
|
||||
|
||||
int mr_book_count(const mr_book *b)
|
||||
{
|
||||
if (!mr_book_valid(b)) return 0;
|
||||
int n = b->ops->count(b->ctx);
|
||||
return n > 0 ? n : 0;
|
||||
}
|
||||
|
||||
int mr_book_meta(const mr_book *b, int index, int *w, int *h)
|
||||
{
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!mr_book_valid(b) || !b->ops->meta) return 0;
|
||||
if (index < 0 || index >= mr_book_count(b)) return 0;
|
||||
return b->ops->meta(b->ctx, index, w, h) ? 1 : 0;
|
||||
}
|
||||
|
||||
meow_img *mr_book_open(mr_book *b, int index)
|
||||
{
|
||||
if (!mr_book_valid(b)) return NULL;
|
||||
if (index < 0 || index >= mr_book_count(b)) return NULL; /* 越界:契约边界 */
|
||||
return b->ops->open(b->ctx, index);
|
||||
}
|
||||
|
||||
void mr_book_close(mr_book *b, meow_img *img)
|
||||
{
|
||||
if (!img) return;
|
||||
if (!mr_book_valid(b) || !b->ops->close) return;
|
||||
b->ops->close(b->ctx, img);
|
||||
}
|
||||
|
||||
void mr_book_destroy(mr_book *b)
|
||||
{
|
||||
if (!b) return;
|
||||
if (b->ops && b->ops->destroy && b->ctx) b->ops->destroy(b->ctx);
|
||||
b->ops = NULL;
|
||||
b->ctx = NULL;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 目录册 */
|
||||
/* ============================================================ */
|
||||
static int name_cmp_qsort(const void *pa, const void *pb)
|
||||
{
|
||||
const char *a = *(const char *const *)pa;
|
||||
const char *b = *(const char *const *)pb;
|
||||
return mr_natural_cmp(a, b);
|
||||
}
|
||||
|
||||
void mr_dir_book_path(const mr_dir_book *db, int index, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return;
|
||||
out[0] = '\0';
|
||||
if (!db || index < 0 || index >= db->count || !db->names[index]) return;
|
||||
size_t dl = strlen(db->dir);
|
||||
if (dl > 0 && db->dir[dl - 1] == '/')
|
||||
snprintf(out, (size_t)outsz, "%s%s", db->dir, db->names[index]);
|
||||
else
|
||||
snprintf(out, (size_t)outsz, "%s/%s", db->dir, db->names[index]);
|
||||
}
|
||||
|
||||
void mr_dir_book_free(mr_dir_book *db)
|
||||
{
|
||||
if (!db) return;
|
||||
for (int i = 0; i < db->count; i++) free(db->names[i]);
|
||||
free(db->names);
|
||||
db->names = NULL;
|
||||
db->count = 0;
|
||||
}
|
||||
|
||||
int mr_dir_book_init(mr_dir_book *db, const char *dir, int maxdim,
|
||||
mr_book_load_fn load, mr_book_unload_fn unload, void *ud)
|
||||
{
|
||||
if (!db) return -1;
|
||||
memset(db, 0, sizeof *db);
|
||||
if (!dir || !dir[0] || !load || !unload) return -1;
|
||||
if (maxdim <= 0) maxdim = 720;
|
||||
if (strlen(dir) >= sizeof db->dir) return -1;
|
||||
|
||||
DIR *d = opendir(dir);
|
||||
if (!d) return -1;
|
||||
|
||||
snprintf(db->dir, sizeof db->dir, "%s", dir);
|
||||
db->maxdim = maxdim;
|
||||
db->load = load;
|
||||
db->unload = unload;
|
||||
db->ud = ud;
|
||||
|
||||
int cap = 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
const char *name = de->d_name;
|
||||
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
|
||||
if (name[0] == '.') continue; /* 隐藏文件不参与成册 */
|
||||
if (!mr_path_is_image(name)) continue;
|
||||
|
||||
char full[MR_PATH_MAX];
|
||||
size_t dl = strlen(db->dir);
|
||||
int n = (dl > 0 && db->dir[dl - 1] == '/')
|
||||
? snprintf(full, sizeof full, "%s%s", db->dir, name)
|
||||
: snprintf(full, sizeof full, "%s/%s", db->dir, name);
|
||||
if (n <= 0 || n >= (int)sizeof full) continue;
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0 || !S_ISREG(st.st_mode)) continue; /* 跳过符号链接/目录 */
|
||||
|
||||
if (db->count >= MR_BOOK_MAX_PAGES) break;
|
||||
if (db->count >= cap) {
|
||||
int nc = cap ? cap * 2 : 32;
|
||||
char **nn = (char **)realloc(db->names, (size_t)nc * sizeof *nn);
|
||||
if (!nn) { mr_dir_book_free(db); closedir(d); return -1; }
|
||||
db->names = nn;
|
||||
cap = nc;
|
||||
}
|
||||
size_t len = strlen(name);
|
||||
db->names[db->count] = (char *)malloc(len + 1);
|
||||
if (!db->names[db->count]) { mr_dir_book_free(db); closedir(d); return -1; }
|
||||
memcpy(db->names[db->count], name, len + 1);
|
||||
db->count++;
|
||||
}
|
||||
closedir(d);
|
||||
|
||||
if (db->count > 1)
|
||||
qsort(db->names, (size_t)db->count, sizeof *db->names, name_cmp_qsort);
|
||||
return db->count;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 图片头探测(不解码像素) */
|
||||
/* ------------------------------------------------------------
|
||||
* 同一套解析同时服务「文件」(目录册,流式读不占内存)和「内存缓冲」
|
||||
* (档案册:page_cache 解压后直接探测,不必再落盘读文件)。
|
||||
* ============================================================ */
|
||||
typedef struct probe_src {
|
||||
const unsigned char *mem; /* 非 NULL = 内存源 */
|
||||
size_t len, pos;
|
||||
FILE *f; /* mem == NULL 时用 */
|
||||
} probe_src;
|
||||
|
||||
static int ps_read(probe_src *s, void *buf, size_t n)
|
||||
{
|
||||
if (s->mem) {
|
||||
if (n > s->len - s->pos) return 0;
|
||||
memcpy(buf, s->mem + s->pos, n);
|
||||
s->pos += n;
|
||||
return 1;
|
||||
}
|
||||
return fread(buf, 1, n, s->f) == n;
|
||||
}
|
||||
|
||||
static int ps_getc(probe_src *s)
|
||||
{
|
||||
if (s->mem) {
|
||||
if (s->pos >= s->len) return EOF;
|
||||
return s->mem[s->pos++];
|
||||
}
|
||||
return fgetc(s->f);
|
||||
}
|
||||
|
||||
static int ps_skip(probe_src *s, long n)
|
||||
{
|
||||
if (n < 0) return 0;
|
||||
if (s->mem) {
|
||||
if ((size_t)n > s->len - s->pos) return 0;
|
||||
s->pos += (size_t)n;
|
||||
return 1;
|
||||
}
|
||||
return fseek(s->f, n, SEEK_CUR) == 0;
|
||||
}
|
||||
|
||||
static unsigned probe_be32(const unsigned char *p)
|
||||
{
|
||||
return ((unsigned)p[0] << 24) | ((unsigned)p[1] << 16) | ((unsigned)p[2] << 8) | p[3];
|
||||
}
|
||||
|
||||
static int probe_png(probe_src *s, mr_img_info *in)
|
||||
{
|
||||
unsigned char hdr[24];
|
||||
if (!ps_read(s, hdr, sizeof hdr)) return 0;
|
||||
static const unsigned char sig[8] = { 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A };
|
||||
if (memcmp(hdr, sig, 8) != 0 || memcmp(hdr + 12, "IHDR", 4) != 0) return 0;
|
||||
unsigned ww = probe_be32(hdr + 16), hh = probe_be32(hdr + 20);
|
||||
if (ww == 0 || hh == 0) return 0;
|
||||
/* IHDR 的位深/颜色类型/压缩/滤波/交错(紧跟宽高之后)。截断头(stb 也读不下去)
|
||||
* 按 RGB 8bit 记;但后端支持字段保持「未知(0)」→ 保守回退 stb 门控。 */
|
||||
int depth = 8, ctype = 2;
|
||||
unsigned char ex[5];
|
||||
if (ps_read(s, ex, sizeof ex)) {
|
||||
depth = ex[0];
|
||||
ctype = ex[1];
|
||||
in->depth = ex[0];
|
||||
in->color = ex[1];
|
||||
in->compression = ex[2];
|
||||
in->filter = ex[3];
|
||||
in->interlace = ex[4];
|
||||
}
|
||||
int ch;
|
||||
switch (ctype) {
|
||||
case 0: ch = 1; break; /* 灰度 */
|
||||
case 2: ch = 3; break; /* RGB */
|
||||
case 3: ch = 3; break; /* 调色板:stb 展开成 RGB(A) */
|
||||
case 4: ch = 2; break; /* 灰度+alpha */
|
||||
case 6: ch = 4; break; /* RGBA */
|
||||
default: ch = 3; break;
|
||||
}
|
||||
if (ww > 0x7FFFFFu || hh > 0x7FFFFFu) return 0; /* 防 int 溢出 */
|
||||
int bpc = (depth == 16) ? 2 : 1;
|
||||
in->w = (int)ww;
|
||||
in->h = (int)hh;
|
||||
in->codec = MR_IMG_CODEC_PNG;
|
||||
in->channels = ch;
|
||||
in->progressive = 0;
|
||||
/* ch==4:stb 直接解到目标 RGBA 缓冲(就是最终输出),中间面记 0。
|
||||
* 其余:stb 先解出 ch 通道源缓冲,再 convert_format 复制一张 RGBA。 */
|
||||
in->planes = (ch == 4) ? 0 : (long long)ww * hh * ch * bpc;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int probe_jpeg(probe_src *s, mr_img_info *in)
|
||||
{
|
||||
unsigned char b[2];
|
||||
if (!ps_read(s, b, 2) || b[0] != 0xFF || b[1] != 0xD8) return 0;
|
||||
for (;;) {
|
||||
int c = ps_getc(s);
|
||||
if (c == EOF) return 0;
|
||||
if (c != 0xFF) continue;
|
||||
int m = ps_getc(s);
|
||||
while (m == 0xFF) m = ps_getc(s); /* 填充字节 */
|
||||
if (m == EOF) return 0;
|
||||
if (m == 0xD8 || m == 0x01 || (m >= 0xD0 && m <= 0xD7)) continue; /* 无长度段 */
|
||||
if (m == 0xD9 || m == 0xDA) return 0; /* EOI/SOS 前没等到 SOF */
|
||||
unsigned char lb[2];
|
||||
if (!ps_read(s, lb, 2)) return 0;
|
||||
int len = ((int)lb[0] << 8) | lb[1];
|
||||
if (len < 2) return 0;
|
||||
int is_sof = (m >= 0xC0 && m <= 0xCF) && m != 0xC4 && m != 0xC8 && m != 0xCC;
|
||||
if (is_sof) {
|
||||
/* 5 字节就够拿宽高(精度 + 高 + 宽);分量数与采样因子能读就读。
|
||||
* 只带最小 SOF 的合成头(宿主测试夹具)也必须能取到尺寸——尺寸探测是
|
||||
* M4 起的硬契约,不能因为预算模型要更多信息就退化。 */
|
||||
unsigned char so[5];
|
||||
if (len < 7 || !ps_read(s, so, sizeof so)) return 0;
|
||||
int hh = ((int)so[1] << 8) | so[2];
|
||||
int ww = ((int)so[3] << 8) | so[4];
|
||||
if (ww <= 0 || hh <= 0) return 0;
|
||||
int prog = (m == 0xC2 || m == 0xC6 || m == 0xCA || m == 0xCE);
|
||||
in->w = ww;
|
||||
in->h = hh;
|
||||
in->codec = MR_IMG_CODEC_JPEG;
|
||||
in->progressive = prog;
|
||||
in->channels = 3;
|
||||
in->sof = m; /* M27:0xC0/0xC1/0xC2 才可能走后端 */
|
||||
in->depth = so[0]; /* 精度(8 才可能走后端) */
|
||||
/* 未知采样因子时按 4:4:4 估(3·W·H,偏保守):宁可多算峰值,不要低估。 */
|
||||
in->planes = (long long)ww * hh * 3;
|
||||
unsigned char nc = 0;
|
||||
if (ps_read(s, &nc, 1) && nc >= 1 && nc <= 4 && len >= 8 + 3 * nc) {
|
||||
int hv[4] = { 0, 0, 0, 0 };
|
||||
int tq[4] = { 0, 0, 0, 0 };
|
||||
int hmax = 1, vmax = 1, ok = 1;
|
||||
for (int i = 0; i < nc && ok; i++) {
|
||||
unsigned char comp[3]; /* id / 采样因子 hv / 量化表 */
|
||||
if (!ps_read(s, comp, sizeof comp)) { ok = 0; break; }
|
||||
hv[i] = comp[1];
|
||||
tq[i] = comp[2];
|
||||
int ch = hv[i] >> 4, cv = hv[i] & 15;
|
||||
if (ch < 1 || ch > 4 || cv < 1 || cv > 4) { ok = 0; break; }
|
||||
if (ch > hmax) hmax = ch;
|
||||
if (cv > vmax) vmax = cv;
|
||||
}
|
||||
for (int i = 0; ok && i < nc; i++) {
|
||||
if (hmax % (hv[i] >> 4) != 0 || vmax % (hv[i] & 15) != 0) ok = 0;
|
||||
}
|
||||
if (ok) {
|
||||
/* M27:给后端支持判定留原始采样因子/量化表号(4 分量已在上面排除)。 */
|
||||
in->ncomp = nc;
|
||||
for (int i = 0; i < nc; i++) {
|
||||
in->comp_h[i] = (unsigned char)(hv[i] >> 4);
|
||||
in->comp_v[i] = (unsigned char)(hv[i] & 15);
|
||||
in->comp_tq[i] = (unsigned char)tq[i];
|
||||
}
|
||||
/* 分量面字节 = 与 stb 完全同式的 w2×h2(MCU 对齐 + 采样因子);
|
||||
* 渐进式每个分量还多一张 w2×h2×2 的 short 系数面(M20 实测 +23.5MB 的来源)。 */
|
||||
long long mcu_x = (ww + 8 * hmax - 1) / (8 * hmax);
|
||||
long long mcu_y = (hh + 8 * vmax - 1) / (8 * vmax);
|
||||
long long planes = 0;
|
||||
for (int i = 0; i < nc; i++) {
|
||||
long long w2 = mcu_x * (hv[i] >> 4) * 8;
|
||||
long long h2 = mcu_y * (hv[i] & 15) * 8;
|
||||
planes += w2 * h2;
|
||||
if (prog) planes += w2 * h2 * 2;
|
||||
}
|
||||
in->channels = nc;
|
||||
in->planes = planes;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (!ps_skip(s, len - 2)) return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int probe_bmp(probe_src *s, mr_img_info *in)
|
||||
{
|
||||
unsigned char hdr[26];
|
||||
if (!ps_read(s, hdr, sizeof hdr)) return 0;
|
||||
if (hdr[0] != 'B' || hdr[1] != 'M') return 0;
|
||||
int ww = (int)((unsigned)hdr[18] | ((unsigned)hdr[19] << 8) |
|
||||
((unsigned)hdr[20] << 16) | ((unsigned)hdr[21] << 24));
|
||||
int hh = (int)((unsigned)hdr[22] | ((unsigned)hdr[23] << 8) |
|
||||
((unsigned)hdr[24] << 16) | ((unsigned)hdr[25] << 24));
|
||||
if (hh < 0) hh = -hh; /* 自底向上 BMP */
|
||||
if (ww <= 0 || hh <= 0) return 0;
|
||||
unsigned char ex[8]; /* biPlanes(2) / biBitCount(2) / biCompression(4) */
|
||||
int bpp = 24;
|
||||
int have_ex = ps_read(s, ex, sizeof ex);
|
||||
if (have_ex) bpp = (int)((unsigned)ex[2] | ((unsigned)ex[3] << 8));
|
||||
in->w = ww;
|
||||
in->h = hh;
|
||||
in->codec = MR_IMG_CODEC_BMP;
|
||||
in->channels = bpp >= 8 ? bpp / 8 : 4; /* stb 一律解到 RGBA 目标 */
|
||||
in->progressive = 0;
|
||||
in->planes = 0; /* stb 直接写 RGBA 目标,不另开源缓冲 */
|
||||
/* M27:后端支持判定(DIB≥40 / BI_RGB / 8·16·24·32bpp)。字段读不全就保持未知 → stb。 */
|
||||
in->dib = (int)((unsigned)hdr[14] | ((unsigned)hdr[15] << 8) |
|
||||
((unsigned)hdr[16] << 16) | ((unsigned)hdr[17] << 24));
|
||||
if (have_ex) {
|
||||
in->depth = bpp;
|
||||
in->compression = (int)((unsigned)ex[4] | ((unsigned)ex[5] << 8) |
|
||||
((unsigned)ex[6] << 16) | ((unsigned)ex[7] << 24));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int probe_any(probe_src *s, mr_img_info *in)
|
||||
{
|
||||
memset(in, 0, sizeof *in);
|
||||
for (int pass = 0; pass < 3; pass++) {
|
||||
s->pos = 0;
|
||||
if (s->f) rewind(s->f);
|
||||
int ok = (pass == 0) ? probe_png(s, in)
|
||||
: (pass == 1) ? probe_jpeg(s, in)
|
||||
: probe_bmp(s, in);
|
||||
if (ok) return 1;
|
||||
memset(in, 0, sizeof *in);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mr_probe_info_mem(const void *buf, size_t len, mr_img_info *info)
|
||||
{
|
||||
if (info) memset(info, 0, sizeof *info);
|
||||
if (!buf || len < 8 || !info) return 0;
|
||||
probe_src s = { (const unsigned char *)buf, len, 0, NULL };
|
||||
if (!probe_any(&s, info) || info->w <= 0 || info->h <= 0) {
|
||||
memset(info, 0, sizeof *info);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_probe_info(const char *path, mr_img_info *info)
|
||||
{
|
||||
if (info) memset(info, 0, sizeof *info);
|
||||
if (!path || !path[0] || !info) return 0;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return 0;
|
||||
probe_src s = { NULL, 0, 0, f };
|
||||
int ok = probe_any(&s, info);
|
||||
fclose(f);
|
||||
if (!ok || info->w <= 0 || info->h <= 0) {
|
||||
memset(info, 0, sizeof *info);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_probe_size_mem(const void *buf, size_t len, int *w, int *h)
|
||||
{
|
||||
mr_img_info in;
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!mr_probe_info_mem(buf, len, &in)) return 0;
|
||||
if (w) *w = in.w;
|
||||
if (h) *h = in.h;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_probe_size(const char *path, int *w, int *h)
|
||||
{
|
||||
mr_img_info in;
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!mr_probe_info(path, &in)) return 0;
|
||||
if (w) *w = in.w;
|
||||
if (h) *h = in.h;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 解码峰值模型 + 内存门控(M27:唯一实现) */
|
||||
/* ------------------------------------------------------------
|
||||
* 阅读器与页源都只调 reader_estimate_peak()——模型口径、预算策略、门控顺序都只在这里
|
||||
* 写一份。两个模型:
|
||||
* 后端(M23):峰值 = 目标图字节 + 后端工作面(JPEG 分量平面 / PNG·BMP 行缓冲)+ 固定开销。
|
||||
* 与源图分辨率/压缩体积无关;命中即放行(不做内存门控)。
|
||||
* stb(回退):M22 的真实分配模型(planes + 抽取输出 + 精修/落框临时图)。
|
||||
* 只有这类页参与 3MB 峰值预算与 MemAvailable < 6MB 门控。
|
||||
* M32(页槽改 RGB565):解码仍产出 RGBA 的 meow_img,阅读器把它转成 565 落槽再释放 RGBA。
|
||||
* 这笔「落槽暂存」= 2B/px(转换时 RGBA 与 565 同时存活)单独记在 convert_bytes,与
|
||||
* peak_bytes 相加才是「一次取页」的真实内存;stb 门控与阅读器日志都按两者之和算。
|
||||
* 模型本身(RGBA 解码分配)不变——变的只是每页常驻从 4B/px 降到 2B/px。
|
||||
* ============================================================ */
|
||||
#define MR_PEAK_CAP_BYTES (3LL * 1024 * 1024) /* 常态单次解码峰值上限 */
|
||||
#define MR_PEAK_HARD_BYTES (12LL * 1024 * 1024) /* MemFree 富余时的绝对上限 */
|
||||
#define MR_MEM_FREE_MARGIN_KB (4L * 1024) /* 放宽档:MemFree 需比峰值多这么多 */
|
||||
#define MR_MEM_FLOOR_KB (6L * 1024) /* 回退路径:MemAvailable 下限 */
|
||||
#define MR_DECODE_SLACK (64LL * 1024) /* 结构体/行缓冲/分配器开销余量 */
|
||||
#define MR_BACKEND_SOFT_CAP (2400LL * 1024) /* 与 meow_jpeg.c 的 2.4MB 预算同值 */
|
||||
#define MR_BACKEND_HARD_CAP (8LL * 1024 * 1024) /* meow_jpeg.c 的 MJ_PEAK_CAP */
|
||||
|
||||
long long reader_peak_budget_bytes(long memfree_kb)
|
||||
{
|
||||
long long budget = MR_PEAK_CAP_BYTES;
|
||||
if (memfree_kb >= 0) {
|
||||
long long room = (memfree_kb - MR_MEM_FREE_MARGIN_KB) * 1024;
|
||||
if (room > budget) budget = room;
|
||||
if (budget > MR_PEAK_HARD_BYTES) budget = MR_PEAK_HARD_BYTES;
|
||||
}
|
||||
return budget;
|
||||
}
|
||||
|
||||
/* 与 meow_img_target_dims / meow_img_downscale 同一公式(纯 C 层不得依赖 SDK)。 */
|
||||
static void mr_target_dims(int w, int h, int maxdim, int *tw, int *th)
|
||||
{
|
||||
if (w >= h) {
|
||||
*tw = maxdim;
|
||||
*th = (int)((long)h * maxdim / w);
|
||||
} else {
|
||||
*th = maxdim;
|
||||
*tw = (int)((long)w * maxdim / h);
|
||||
}
|
||||
if (*tw < 1) *tw = 1;
|
||||
if (*th < 1) *th = 1;
|
||||
}
|
||||
|
||||
static long long mr_ceil_div(long long a, long long b) { return (a + b - 1) / b; }
|
||||
|
||||
/* 档位折算:与 meow_img_load_file_step 的 eff 计算逐字一致
|
||||
* (eff = min(maxdim, ceil(长边/step)),后端按 eff 选 DCT 阶)。 */
|
||||
static int mr_effective_dim(int longside, int step, int dim)
|
||||
{
|
||||
if (step < 1) step = 1;
|
||||
if (step > 8) step = 8;
|
||||
int eff = dim;
|
||||
int cap = (int)mr_ceil_div(longside, step);
|
||||
if (cap > 0 && cap < eff) eff = cap;
|
||||
return eff;
|
||||
}
|
||||
|
||||
/* PNG 位深 × 颜色类型 → 每像素位数(与 meow_png_stream.c 的 png_bits_of 同式)。 */
|
||||
static int mr_png_bits(int color, int depth)
|
||||
{
|
||||
switch (color) {
|
||||
case 0: return depth;
|
||||
case 2: return depth * 3;
|
||||
case 3: return depth;
|
||||
case 4: return depth * 2;
|
||||
case 6: return depth * 4;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 位深合法性(与 meow_png_stream.c 的 png_depth_ok **逐字同式**,P2-1):
|
||||
* color 0/3 允许 1/2/4/8(灰度再加 16),color 2/4/6 只允许 8/16。 */
|
||||
static int mr_png_depth_ok(int color, int depth)
|
||||
{
|
||||
if (color == 0) return depth == 1 || depth == 2 || depth == 4 || depth == 8 || depth == 16;
|
||||
if (color == 3) return depth == 1 || depth == 2 || depth == 4 || depth == 8;
|
||||
if (color == 2 || color == 4 || color == 6) return depth == 8 || depth == 16;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 后端支持矩阵(镜像 meow_jpeg.c / meow_png_stream.c 的拒绝条件;字段未知一律不支持)。
|
||||
* eff 是后端实际会收到的 maxdim(见 mr_effective_dim)。 */
|
||||
static int mr_backend_supported(const mr_img_info *in, int eff)
|
||||
{
|
||||
if (!in || in->w <= 0 || in->h <= 0 || eff <= 0) return 0;
|
||||
int longside = in->w > in->h ? in->w : in->h;
|
||||
if (longside <= eff) return 0; /* 小图走旧入口 */
|
||||
|
||||
switch (in->codec) {
|
||||
case MR_IMG_CODEC_JPEG: {
|
||||
if (in->sof != 0xC0 && in->sof != 0xC1 && in->sof != 0xC2) return 0;
|
||||
if (in->depth != 8) return 0;
|
||||
if (in->ncomp < 1 || in->ncomp > 3) return 0;
|
||||
for (int i = 0; i < in->ncomp; i++) {
|
||||
if (in->comp_h[i] < 1 || in->comp_h[i] > 4) return 0;
|
||||
if (in->comp_v[i] < 1 || in->comp_v[i] > 4) return 0;
|
||||
if (in->comp_tq[i] > 3) return 0;
|
||||
}
|
||||
int n = (int)mr_ceil_div(8LL * eff, longside);
|
||||
if (n < 1) n = 1;
|
||||
return n <= 7; /* N=8 起几乎没有缩解收益 → stb */
|
||||
}
|
||||
case MR_IMG_CODEC_PNG: {
|
||||
if (in->interlace || in->compression || in->filter) return 0;
|
||||
if (in->w > 65535 || in->h > 65535) return 0;
|
||||
if (!mr_png_depth_ok(in->color, in->depth)) return 0;
|
||||
return 1;
|
||||
}
|
||||
case MR_IMG_CODEC_BMP:
|
||||
if (in->dib < 40 || in->dib > 1024) return 0;
|
||||
if (in->compression != 0) return 0; /* RLE/BITFIELDS → stb */
|
||||
return in->depth == 8 || in->depth == 16 || in->depth == 24 || in->depth == 32;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* JPEG 分量平面字节(与 meow_jpeg.c 的 mj_layout 同式,n[] 是各分量的 DCT 阶)。 */
|
||||
static long long mr_jpeg_plane_bytes(const mr_img_info *in, const int n[3])
|
||||
{
|
||||
int hmax = 1, vmax = 1;
|
||||
for (int i = 0; i < in->ncomp; i++) {
|
||||
if (in->comp_h[i] > hmax) hmax = in->comp_h[i];
|
||||
if (in->comp_v[i] > vmax) vmax = in->comp_v[i];
|
||||
}
|
||||
long long mcux = mr_ceil_div(in->w, 8LL * hmax);
|
||||
long long mcuy = mr_ceil_div(in->h, 8LL * vmax);
|
||||
long long total = 0;
|
||||
for (int i = 0; i < in->ncomp; i++) {
|
||||
long long sw = mr_ceil_div(in->w * (long long)in->comp_h[i], hmax);
|
||||
long long sh = mr_ceil_div(in->h * (long long)in->comp_v[i], vmax);
|
||||
long long ow = mr_ceil_div(sw, 8);
|
||||
long long oh = mr_ceil_div(sh, 8);
|
||||
long long bx = mcux * in->comp_h[i];
|
||||
long long by = mcuy * in->comp_v[i];
|
||||
if (bx < ow) bx = ow;
|
||||
if (by < oh) by = oh;
|
||||
total += bx * (long long)n[i] * by * (long long)n[i];
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/* 后端峰值:目标图 + 工作面 + 固定开销。JPEG 的两步降档(色度减半、2.4MB 预算、
|
||||
* 8MB 硬上限)与 meow_jpeg.c 一致——估算因此能贴着实测,而不是靠放大余量。 */
|
||||
static long long mr_backend_peak_bytes(const mr_img_info *in, int eff, int step,
|
||||
int box_w, int box_h, int *tw, int *th)
|
||||
{
|
||||
int dim = box_w > box_h ? box_w : box_h;
|
||||
mr_target_dims(in->w, in->h, eff, tw, th);
|
||||
long long target = (long long)*tw * (*th) * 4;
|
||||
long long work = 0;
|
||||
|
||||
if (in->codec == MR_IMG_CODEC_JPEG) {
|
||||
int longside = in->w > in->h ? in->w : in->h;
|
||||
int base = (int)mr_ceil_div(8LL * eff, longside);
|
||||
if (base < 1) base = 1;
|
||||
int n[3] = { base, base, base };
|
||||
work = mr_jpeg_plane_bytes(in, n);
|
||||
/* 4:4:4 色度与亮度等大:亮度平面超过 400KB 时色度降一档(mj_sof 同式)。 */
|
||||
if (in->ncomp == 3 && in->comp_h[0] == in->comp_h[1] && in->comp_v[0] == in->comp_v[1] &&
|
||||
work / 3 > 400LL * 1024) {
|
||||
for (int i = 1; i < 3; i++) if (n[i] > 1) n[i] >>= 1;
|
||||
work = mr_jpeg_plane_bytes(in, n);
|
||||
}
|
||||
/* 平面 + 目标图超 2.4MB:先降色度(亮度保持清晰)。 */
|
||||
if (work + target > MR_BACKEND_SOFT_CAP) {
|
||||
int changed = 0;
|
||||
for (int i = 1; i < 3; i++) if (n[i] > 1) { n[i] >>= 1; changed = 1; }
|
||||
if (changed) work = mr_jpeg_plane_bytes(in, n);
|
||||
}
|
||||
/* 最后一道防线:异常巨图整体降档(最多 N=1)。 */
|
||||
while (work + target > MR_BACKEND_HARD_CAP && base > 1) {
|
||||
base >>= 1;
|
||||
for (int i = 0; i < 3; i++) if (n[i] > base) n[i] = base;
|
||||
work = mr_jpeg_plane_bytes(in, n);
|
||||
}
|
||||
} else if (in->codec == MR_IMG_CODEC_PNG) {
|
||||
long long rowbytes = (in->w * (long long)mr_png_bits(in->color, in->depth) + 7) / 8;
|
||||
work = 3 * rowbytes + 1; /* line + prev + cur(png_stream 同式) */
|
||||
work += 48LL * 1024; /* inflate 状态余量(zlib 窗口等) */
|
||||
} else { /* BMP */
|
||||
int bpp = in->depth > 0 ? in->depth : 24;
|
||||
long long rowbytes = (in->w * (long long)bpp + 31) / 32 * 4;
|
||||
work = rowbytes; /* 单行 seek 缓冲 */
|
||||
work += 48LL * 1024; /* 结构体/调色板/stdio 余量 */
|
||||
}
|
||||
|
||||
long long peak = target + work + MR_DECODE_SLACK;
|
||||
|
||||
/* 阅读器在「粗档位 + 输出尺寸与显示框不一致」时还会拷一张显示框大小的图;
|
||||
* 条件与 reader_load_page 逐字对齐,避免把常态误加一倍。 */
|
||||
if (step > 1 && dim > 0 && (in->w >= box_w || in->h >= box_h) &&
|
||||
(*tw != box_w || *th != box_h)) {
|
||||
peak += (long long)box_w * box_h * 4;
|
||||
}
|
||||
return peak;
|
||||
}
|
||||
|
||||
/* stb 回退路径峰值(M22 模型 + M27 Adam7 修正):只有这类页才参与预算门控。 */
|
||||
static long long mr_stb_peak_bytes(const mr_img_info *in, int step, int box_w, int box_h)
|
||||
{
|
||||
long long W = in->w, H = in->h;
|
||||
long long box = (long long)box_w * box_h * 4;
|
||||
long long dst;
|
||||
if (in->codec == MR_IMG_CODEC_JPEG) {
|
||||
if (step < 1) step = 1;
|
||||
dst = mr_ceil_div(W, step) * mr_ceil_div(H, step) * 4;
|
||||
} else {
|
||||
dst = W * H * 4; /* PNG/BMP:stb 无解码期缩放,整幅落地 */
|
||||
}
|
||||
long long peak = in->planes + dst; /* stb 阶段 */
|
||||
/* Adam7 交错 PNG:stb 额外整幅暂存各 pass 的 raw 与一张中间 RGBA(实测 1600×2400
|
||||
* 45MB,M22 模型只算 26MB)。多算 planes+dst 保证是上界;这类页无论预算如何都超过
|
||||
* 12MB 放宽档,结果仍是占位(安全侧)。 */
|
||||
if (in->codec == MR_IMG_CODEC_PNG && in->interlace)
|
||||
peak += in->planes + dst;
|
||||
long long finished = dst > box ? box : dst;
|
||||
if (dst + finished > peak) peak = dst + finished;
|
||||
if (finished + box > peak) peak = finished + box;
|
||||
return peak + MR_DECODE_SLACK;
|
||||
}
|
||||
|
||||
void reader_estimate_peak(const mr_img_info *in, int step, int box_w, int box_h,
|
||||
long memfree_kb, long memavail_kb, mr_decode_decision *out)
|
||||
{
|
||||
if (!out) return;
|
||||
out->path = MR_DECODE_PATH_STB;
|
||||
out->skip = MR_DECODE_OK;
|
||||
out->step = 1;
|
||||
out->tw = out->th = 0;
|
||||
out->peak_bytes = 0;
|
||||
out->convert_bytes = 0;
|
||||
out->budget_bytes = reader_peak_budget_bytes(memfree_kb);
|
||||
if (!in || in->w <= 0 || in->h <= 0 || box_w <= 0 || box_h <= 0) return;
|
||||
|
||||
if (step < 1) step = 1;
|
||||
if (step > 8) step = 8;
|
||||
out->step = step;
|
||||
|
||||
int dim = box_w > box_h ? box_w : box_h;
|
||||
int longside = in->w > in->h ? in->w : in->h;
|
||||
int eff = mr_effective_dim(longside, step, dim);
|
||||
|
||||
/* 1) 先看后端能否接住:命中时峰值与源尺寸无关(典型页 <2.6MB),不做 3MB 常态门控;
|
||||
* 但仍保留两道安全阀(P2-2):MemAvailable <6MB 直接占位、估算超过 12MB 硬上限占位。
|
||||
* 后端路由由 reader 用 backend-only 入口执行(P1-1),失败即坏图,绝不回落 stb。 */
|
||||
if (mr_backend_supported(in, eff)) {
|
||||
out->path = MR_DECODE_PATH_BACKEND;
|
||||
out->peak_bytes = mr_backend_peak_bytes(in, eff, step, box_w, box_h,
|
||||
&out->tw, &out->th);
|
||||
out->convert_bytes = (long long)out->tw * out->th * 2; /* 565 落槽暂存(M32) */
|
||||
if (memavail_kb >= 0 && memavail_kb < MR_MEM_FLOOR_KB)
|
||||
out->skip = MR_DECODE_SKIP_LOWMEM;
|
||||
else if (out->peak_bytes + out->convert_bytes > MR_PEAK_HARD_BYTES)
|
||||
out->skip = MR_DECODE_SKIP_PEAK;
|
||||
else
|
||||
out->skip = MR_DECODE_OK;
|
||||
return;
|
||||
}
|
||||
|
||||
/* 2) 回退 stb:只有这里才做 MemAvailable 与峰值预算门控。 */
|
||||
if (longside <= dim) { out->tw = in->w; out->th = in->h; }
|
||||
else mr_target_dims(in->w, in->h, dim, &out->tw, &out->th);
|
||||
out->peak_bytes = mr_stb_peak_bytes(in, step, box_w, box_h);
|
||||
out->convert_bytes = (long long)out->tw * out->th * 2; /* 565 落槽暂存(M32) */
|
||||
if (memavail_kb >= 0 && memavail_kb < MR_MEM_FLOOR_KB)
|
||||
out->skip = MR_DECODE_SKIP_LOWMEM;
|
||||
else if (out->peak_bytes + out->convert_bytes > out->budget_bytes)
|
||||
out->skip = MR_DECODE_SKIP_PEAK; /* 565 暂存也占内存:门控按「解码+落槽」总量算 */
|
||||
else
|
||||
out->skip = MR_DECODE_OK;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* mr_book_ops 适配器(目录册) */
|
||||
/* ============================================================ */
|
||||
static int dir_ops_count(void *ctx)
|
||||
{
|
||||
mr_dir_book *db = (mr_dir_book *)ctx;
|
||||
return db ? db->count : 0;
|
||||
}
|
||||
|
||||
static int dir_ops_meta(void *ctx, int index, int *w, int *h)
|
||||
{
|
||||
mr_dir_book *db = (mr_dir_book *)ctx;
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!db || index < 0 || index >= db->count) return 0;
|
||||
char path[MR_PATH_MAX];
|
||||
mr_dir_book_path(db, index, path, (int)sizeof path);
|
||||
return mr_probe_size(path, w, h);
|
||||
}
|
||||
|
||||
static meow_img *dir_ops_open(void *ctx, int index)
|
||||
{
|
||||
mr_dir_book *db = (mr_dir_book *)ctx;
|
||||
if (!db || index < 0 || index >= db->count) return NULL;
|
||||
char path[MR_PATH_MAX];
|
||||
mr_dir_book_path(db, index, path, (int)sizeof path);
|
||||
if (!path[0]) return NULL;
|
||||
return db->load(path, db->ud, db->maxdim);
|
||||
}
|
||||
|
||||
static void dir_ops_close(void *ctx, meow_img *img)
|
||||
{
|
||||
mr_dir_book *db = (mr_dir_book *)ctx;
|
||||
if (db && db->unload && img) db->unload(img);
|
||||
}
|
||||
|
||||
static void dir_ops_destroy(void *ctx)
|
||||
{
|
||||
mr_dir_book *db = (mr_dir_book *)ctx;
|
||||
if (db) mr_dir_book_free(db);
|
||||
}
|
||||
|
||||
static const mr_book_ops g_dir_ops = {
|
||||
dir_ops_count, dir_ops_meta, dir_ops_open, dir_ops_close, dir_ops_destroy,
|
||||
};
|
||||
|
||||
void mr_dir_book_bind(mr_book *b, mr_dir_book *db)
|
||||
{
|
||||
if (!b) return;
|
||||
b->ops = db ? &g_dir_ops : NULL;
|
||||
b->ctx = db;
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/* logic/reader/page_source.h — 页源契约(纯 C,不依赖 meow_ui.h)。
|
||||
*
|
||||
* 一册 = 有序页序列。契约只有四件事:
|
||||
* count 总页数
|
||||
* meta(i) 页的原始像素尺寸(可选;条漫连续滚动先用它排版,未知时返回 0)
|
||||
* open(i) 取第 i 页:解码 + downscale 后返回 meow_img*(调用方拥有,必须 close)
|
||||
* close(img) 释放 open 返回的图像
|
||||
*
|
||||
* 两种消费方式共用同一契约,后续实现零改动:
|
||||
* - 左右翻页(本任务):预解码窗口 = 当前 ±1 页,翻页只 blit 窗口内已备好的图;
|
||||
* - 条漫连续滚动(M7):先 meta(i) 累加高度排版,再按滚动位置 open(视口±1 页)。
|
||||
* zip 册(M9/M11)只需另写一套 mr_book_ops,目录册是第一个实现。
|
||||
*
|
||||
* meow_img 在 logic 层是前向声明的不透明句柄;解码/释放由 UI 层注入回调
|
||||
* (logic 不 include SDK,宿主测试用假回调即可覆盖契约边界)。
|
||||
*/
|
||||
#ifndef MR_PAGE_SOURCE_H
|
||||
#define MR_PAGE_SOURCE_H
|
||||
|
||||
#include "logic/library.h" /* mr_path_is_image / mr_natural_cmp / MR_PATH_MAX / MR_NAME_MAX */
|
||||
#include <stddef.h> /* size_t(mr_probe_size_mem) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct meow_img;
|
||||
typedef struct meow_img meow_img; /* SDK 不透明句柄;logic 层只透传 */
|
||||
|
||||
#define MR_BOOK_MAX_PAGES 4096 /* 单册页数上限(防坏卡/巨目录撑爆内存) */
|
||||
|
||||
/* 解码回调(UI 层注入):解码 path,长边 >maxdim 时等比 downscale;失败返回 NULL。
|
||||
* 必须可在后台线程调用(宿主测试用假实现)。 */
|
||||
typedef meow_img *(*mr_book_load_fn)(const char *path, void *ud, int maxdim);
|
||||
/* 释放 open 返回的图像(UI 层注入 meow_img_free;宿主测试用假实现计数)。 */
|
||||
typedef void (*mr_book_unload_fn)(meow_img *img);
|
||||
|
||||
typedef struct mr_book_ops {
|
||||
int (*count)(void *ctx);
|
||||
/* 页原始尺寸(未 downscale);1=已知,0=未知。可为 NULL。 */
|
||||
int (*meta)(void *ctx, int index, int *w, int *h);
|
||||
/* 取页:越界/解码失败返回 NULL。返回值的所有权交给调用方。 */
|
||||
meow_img *(*open)(void *ctx, int index);
|
||||
/* 释放 open 返回的图像;须容忍 NULL。 */
|
||||
void (*close)(void *ctx, meow_img *img);
|
||||
/* 释放 provider 自身(册内索引等)。可为 NULL。 */
|
||||
void (*destroy)(void *ctx);
|
||||
} mr_book_ops;
|
||||
|
||||
typedef struct mr_book {
|
||||
const mr_book_ops *ops;
|
||||
void *ctx;
|
||||
} mr_book;
|
||||
|
||||
/* ---- 契约包装:所有入口对 NULL/越界安全(越界 open 返回 NULL) ---- */
|
||||
int mr_book_valid(const mr_book *b);
|
||||
int mr_book_count(const mr_book *b);
|
||||
int mr_book_meta(const mr_book *b, int index, int *w, int *h);
|
||||
meow_img *mr_book_open(mr_book *b, int index);
|
||||
void mr_book_close(mr_book *b, meow_img *img);
|
||||
/* 释放 provider 自身(不含已 open 的图像——那些由调用方先 close)。幂等。 */
|
||||
void mr_book_destroy(mr_book *b);
|
||||
|
||||
/* ---- 目录册(文件夹即书):目录直接子文件里的图片按自然序成册 ---- */
|
||||
typedef struct mr_dir_book {
|
||||
char dir[MR_PATH_MAX];
|
||||
char **names; /* 自然序文件名(堆分配) */
|
||||
int count;
|
||||
int maxdim; /* open 时传给解码回调的长边上限 */
|
||||
mr_book_load_fn load;
|
||||
mr_book_unload_fn unload;
|
||||
void *ud; /* 解码回调的宿主上下文 */
|
||||
} mr_dir_book;
|
||||
|
||||
/* 扫描 dir 的直接子文件,收图片名并自然排序。
|
||||
* 返回页数(>=0);参数非法 / 目录不可读 / 内存不足返回 -1(db 已清空、可安全 free)。 */
|
||||
int mr_dir_book_init(mr_dir_book *db, const char *dir, int maxdim,
|
||||
mr_book_load_fn load, mr_book_unload_fn unload, void *ud);
|
||||
void mr_dir_book_free(mr_dir_book *db);
|
||||
/* 第 index 页的完整路径;越界时输出空串。 */
|
||||
void mr_dir_book_path(const mr_dir_book *db, int index, char *out, int outsz);
|
||||
/* 把目录册绑定到通用 mr_book(db 生命周期由调用方持有)。 */
|
||||
void mr_dir_book_bind(mr_book *b, mr_dir_book *db);
|
||||
|
||||
/* 纯 C 头解析:取图片原始像素尺寸(不解码像素)。
|
||||
* PNG(IHDR)/ JPEG(SOF0-15)/ BMP(BITMAPINFOHEADER+)。1=已知,0=未知。 */
|
||||
int mr_probe_size(const char *path, int *w, int *h);
|
||||
/* 同上,但从内存缓冲探测(档案页解压后不需要落盘再读一遍)。 */
|
||||
int mr_probe_size_mem(const void *buf, size_t len, int *w, int *h);
|
||||
|
||||
/* ---- 追加(M22):解码内存预算需要的头部信息(仍然零像素内存) ----
|
||||
* 解析器按**内容**嗅探(PNG → JPEG → BMP),不信任扩展名:一个 .jpg 后缀的 PNG 也能
|
||||
* 得到正确结论。planes 是 stb 解码时**除最终 RGBA 输出缓冲以外**的中间面字节数:
|
||||
* - JPEG:各分量面 w2*h2(按 MCU 对齐 + 采样因子,与 stb 同式),渐进式再加 short 系数面;
|
||||
* - PNG :源像素缓冲 ch*W*H(16bit 再 ×2,ch==4 时就是输出缓冲本身,故记 0);
|
||||
* - BMP :0(stb 对 24/32bpp 直接写 RGBA 目标,不另开源缓冲)。
|
||||
* 调用方(阅读器)再叠加「抽取输出 + 精修/落框临时图」得到真实峰值。
|
||||
* 1=已知(info 已填满),0=未知/损坏(info 清零且 codec=MR_IMG_CODEC_UNKNOWN)。 */
|
||||
#define MR_IMG_CODEC_UNKNOWN 0
|
||||
#define MR_IMG_CODEC_JPEG 1
|
||||
#define MR_IMG_CODEC_PNG 2
|
||||
#define MR_IMG_CODEC_BMP 3
|
||||
|
||||
typedef struct mr_img_info {
|
||||
int w, h;
|
||||
int codec; /* MR_IMG_CODEC_* */
|
||||
int progressive; /* 1=渐进式 JPEG(SOF2/SOF6/SOF10/SOF14) */
|
||||
int channels; /* 源通道数(JPEG 分量数 / PNG 颜色类型 / BMP 位深/8) */
|
||||
long long planes; /* 见上:stb 中间面字节(不含最终 RGBA 输出) */
|
||||
|
||||
/* ---- 追加(M27):真缩解后端(M23)支持判定所需的头字段。
|
||||
* 缺省 0 = 未知;判定按「不支持」处理(保守回退 stb 门控),绝不因缺字段误走后端。 ---- */
|
||||
int depth; /* JPEG 精度 / PNG 位深 / BMP 位深(bpp) */
|
||||
int color; /* PNG 颜色类型(0/2/3/4/6);非 PNG 为 0 */
|
||||
int sof; /* JPEG SOF marker(0xC0/0xC1/0xC2 才可能走后端) */
|
||||
int interlace; /* PNG 1=Adam7 交错(后端不支持) */
|
||||
int compression; /* PNG 压缩法 / BMP BI_*(0 才可能走后端) */
|
||||
int filter; /* PNG 滤波法(0 才可能走后端) */
|
||||
int dib; /* BMP DIB 头长度(<40 后端不支持) */
|
||||
int ncomp; /* JPEG 分量数(1..3;4=CMYK → 后端不支持) */
|
||||
unsigned char comp_h[3], comp_v[3], comp_tq[3]; /* JPEG 采样因子/量化表号 */
|
||||
} mr_img_info;
|
||||
|
||||
int mr_probe_info(const char *path, mr_img_info *info);
|
||||
int mr_probe_info_mem(const void *buf, size_t len, mr_img_info *info);
|
||||
|
||||
/* ---- 追加(M27):解码峰值模型 + 门控的唯一实现 ----
|
||||
* 阅读器(ui/reader)与页源(本层)都只调 reader_estimate_peak,不得各写一份模型。
|
||||
* 一次调用同时回答三件事:
|
||||
* path 走后端(MR_DECODE_PATH_BACKEND)还是回退 stb(MR_DECODE_PATH_STB);
|
||||
* skip 是否占位(MR_DECODE_OK / SKIP_LOWMEM / SKIP_PEAK);
|
||||
* peak_bytes + tw/th 预估峰值与产出尺寸(日志/测试直接对照实测)。
|
||||
*
|
||||
* 判定顺序(M27 核心设计):
|
||||
* 1. 头部特征满足 M23 后端支持矩阵(JPEG SOF0/1/2、≤3 分量、8bit、N=1..7;
|
||||
* PNG 非交错/标准压缩;BMP DIB≥40/BI_RGB/8·16·24·32bpp)→ **走后端**:
|
||||
* 峰值 = 目标图字节 + 后端工作面 + 固定开销,与源分辨率无关 → 不占位(不做内存门控)。
|
||||
* 2. 否则回退 stb:按 M22 的 stb 真实分配模型算峰值,超预算 → SKIP_PEAK;
|
||||
* MemAvailable < 6MB → SKIP_LOWMEM。这两道门控只作用于回退路径。
|
||||
*
|
||||
* 参数:step 与 meow_img_load_file_step 同义(JPEG 档位提示);box_w/box_h 是阅读器
|
||||
* 最终显示框;memfree_kb/memavail_kb 读不到时传 -1。out 必须非 NULL。
|
||||
* 估算精度口径:**后端命中页 / 典型页**保证估算 ≥ 实测(--wrap malloc)且 ≤1.5×;
|
||||
* 回退 stb 的小图只保证门控安全(可能保守,不强求 1.5× 上界)。断言见 tuku/test。 */
|
||||
#define MR_DECODE_PATH_STB 0
|
||||
#define MR_DECODE_PATH_BACKEND 1
|
||||
|
||||
#define MR_DECODE_OK 0
|
||||
#define MR_DECODE_SKIP_LOWMEM 1
|
||||
#define MR_DECODE_SKIP_PEAK 2
|
||||
|
||||
typedef struct mr_decode_decision {
|
||||
int path; /* MR_DECODE_PATH_* */
|
||||
int skip; /* MR_DECODE_OK / SKIP_* */
|
||||
int step; /* 实际档位(<=1 按 1) */
|
||||
int tw, th; /* 预估最终产出尺寸(含阅读器落框精修) */
|
||||
long long peak_bytes; /* 预估峰值(字节,含固定开销) */
|
||||
long long budget_bytes; /* 本次预算(字节;SKIP_PEAK 的比较基准) */
|
||||
/* ---- 追加(M32):页槽改 RGB565 后的「落槽暂存」----
|
||||
* 解码仍产出 RGBA 的 meow_img;阅读器把它一次性转成 565 再释放 RGBA,转换期间
|
||||
* RGBA(4B/px)+ 目标 565(2B/px)同时存在。这笔暂存(= 2×tw×th)与解码峰值不同时
|
||||
* 发生(解码中间面此时已释放),但同属「一次取页操作」的内存,必须一起进预算:
|
||||
* page_source 的门控按 peak_bytes + convert_bytes 比较,阅读器也按两者之和做日志/断言
|
||||
* (与 peak_bytes 分开,保持解码模型的原有口径)。 */
|
||||
long long convert_bytes;
|
||||
} mr_decode_decision;
|
||||
|
||||
void reader_estimate_peak(const mr_img_info *in, int step, int box_w, int box_h,
|
||||
long memfree_kb, long memavail_kb, mr_decode_decision *out);
|
||||
|
||||
/* 预算策略(reader_estimate_peak 内部用的就是它):常态 3MB;MemFree 富余时放宽到
|
||||
* min(12MB, MemFree-4MB);读不到 MemFree(<0)只用常态。暴露给日志/测试,避免复制。 */
|
||||
long long reader_peak_budget_bytes(long memfree_kb);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 档案册(M11):.cbz / .zip 当一本书 */
|
||||
/* ------------------------------------------------------------
|
||||
* 目录册之外的第二种 mr_book 实现,契约完全相同。条目名按 zip 原始字节
|
||||
* 透传(通常 UTF-8):只收「图片后缀 + 路径安全 + 非隐藏 + 非目录」的条目,
|
||||
* 跨子目录按 mr_natural_cmp 自然排序。open(i) 先把条目解压进
|
||||
* logic/reader/page_cache(磁盘 LRU),再用注入的解码回调按路径解码——
|
||||
* 与目录册走同一条 UI 解码/预算路径,翻页与条漫两种模式零改动复用。
|
||||
*
|
||||
* 条目名永远不会被拼成文件系统路径(缓存文件名是哈希),路径安全过滤是
|
||||
* 纵深防御:拒绝绝对路径、'..' 段、反斜杠与内嵌 NUL。
|
||||
* ============================================================ */
|
||||
#define MR_ZIP_MAX_ENTRY_NAME 1024 /* 超长条目名跳过(防病态包) */
|
||||
#define MR_ZIP_PAGE_MAX_BYTES (64LL * 1024 * 1024) /* 单页解压上限(防 zip 炸弹) */
|
||||
|
||||
typedef struct mr_zip_book {
|
||||
char path[MR_PATH_MAX];
|
||||
void *arch; /* mz_archive*(zip 层句柄;不对外暴露) */
|
||||
void *lock; /* pthread_mutex_t*:mz_read* 串行化(open/meta 共用) */
|
||||
int *order; /* 页 i → zip 条目下标(堆分配) */
|
||||
int *meta_w; /* 页原始宽/高(按需读条目 4KB 前缀探测;NULL=未探测) */
|
||||
int *meta_h;
|
||||
int count;
|
||||
int maxdim;
|
||||
long long mtime; /* 档案 mtime/size:解压缓存键的一部分 */
|
||||
long long size;
|
||||
mr_book_load_fn load; /* 可为 NULL(只取封面/页路径时不需要解码) */
|
||||
mr_book_unload_fn unload;
|
||||
void *ud;
|
||||
} mr_zip_book;
|
||||
|
||||
/* 打开档案并建立页表。返回页数(>=0);参数非法/坏档/空档案不可读返回 -1
|
||||
* (zb 已清空、可安全 free)。0 页的合法档案返回 0(zb 仍持有打开的 archive)。 */
|
||||
int mr_zip_book_init(mr_zip_book *zb, const char *path, int maxdim,
|
||||
mr_book_load_fn load, mr_book_unload_fn unload, void *ud);
|
||||
void mr_zip_book_free(mr_zip_book *zb);
|
||||
/* 把档案册绑定到通用 mr_book(zb 生命周期由调用方持有)。 */
|
||||
void mr_zip_book_bind(mr_book *b, mr_zip_book *zb);
|
||||
/* 第 index 页在 zip 内的条目名(原始字节);越界输出空串。 */
|
||||
void mr_zip_book_page_name(const mr_zip_book *zb, int index, char *out, int outsz);
|
||||
/* 确保第 index 页已解压到缓存并返回缓存文件路径;越界/坏档/缓存不可用 -1。 */
|
||||
int mr_zip_book_page_path(mr_zip_book *zb, int index, char *out, int outsz);
|
||||
|
||||
/* 只数页(书库扫描/邻册判断用;不解压、不建缓存)。0=不是档案或没有图片页。 */
|
||||
int mr_zip_count_pages(const char *path);
|
||||
/* 档案封面:档内自然序第一张图解到缓存,路径写入 out。1=成功。 */
|
||||
int mr_zip_cover_path(const char *path, char *out, int outsz);
|
||||
/* 条目是否算一页(图片后缀 + 路径安全 + 非隐藏 + 非目录)。1=是。 */
|
||||
int mr_zip_entry_is_page(const char *name, unsigned name_len);
|
||||
|
||||
/* ============================================================ */
|
||||
/* 统一开册(目录册 / 档案册) */
|
||||
/* ------------------------------------------------------------
|
||||
* 书库点入的行既可能是目录也可能是 .cbz/.zip;阅读器只调这里,不关心 provider。
|
||||
* 句柄由调用方提供(阅读器用静态变量),生命周期 = 打开到 close_auto。
|
||||
* ============================================================ */
|
||||
#define MR_BOOK_KIND_NONE 0
|
||||
#define MR_BOOK_KIND_DIR 1
|
||||
#define MR_BOOK_KIND_ZIP 2
|
||||
|
||||
typedef struct mr_book_handle {
|
||||
int kind;
|
||||
union {
|
||||
mr_dir_book dir;
|
||||
mr_zip_book zip;
|
||||
} u;
|
||||
} mr_book_handle;
|
||||
|
||||
/* 按路径自动选 provider 并绑定 b。返回页数(>=0)或 -1(打不开)。
|
||||
* 页数为 0 时 b 不绑定(空册),句柄仍可安全 mr_book_close_auto。 */
|
||||
int mr_book_open_auto(mr_book *b, mr_book_handle *h, const char *path, int maxdim,
|
||||
mr_book_load_fn load, mr_book_unload_fn unload, void *ud);
|
||||
/* 释放 provider(先 mr_book_destroy 再释放各自索引);可重复调用。 */
|
||||
void mr_book_close_auto(mr_book *b, mr_book_handle *h);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* MR_PAGE_SOURCE_H */
|
||||
@@ -0,0 +1,549 @@
|
||||
/* zip.c — zip 容器读取器实现,见 zip.h 的用法/线程/安全说明。 */
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#define _FILE_OFFSET_BITS 64
|
||||
|
||||
#include "zip.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <zlib.h>
|
||||
|
||||
#define MZ_SIG_LOCAL 0x04034b50u
|
||||
#define MZ_SIG_CD 0x02014b50u
|
||||
#define MZ_SIG_EOCD 0x06054b50u
|
||||
#define MZ_LOCAL_MIN 30u
|
||||
#define MZ_CD_MIN 46u
|
||||
#define MZ_EOCD_MIN 22u
|
||||
#define MZ_COMMENT_MAX 65535u
|
||||
#define MZ_IN_CHUNK 65536u
|
||||
#define MZ_PREFIX_IN_CHUNK 4096u /* prefix 读的栈缓冲(M32,见 inflate_entry) */
|
||||
#define MZ_CRC_CHUNK (1u << 24)
|
||||
|
||||
struct mz_archive {
|
||||
FILE *fp;
|
||||
uint64_t file_size;
|
||||
int count;
|
||||
mz_entry *entries;
|
||||
char *names;
|
||||
};
|
||||
|
||||
static uint16_t rd16(const uint8_t *p)
|
||||
{
|
||||
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
||||
}
|
||||
|
||||
static uint32_t rd32(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
||||
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
static int u64_add(uint64_t a, uint64_t b, uint64_t *out)
|
||||
{
|
||||
if (a > UINT64_MAX - b) return 0;
|
||||
*out = a + b;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int seek_to(FILE *fp, uint64_t off)
|
||||
{
|
||||
off_t o = (off_t)off;
|
||||
if ((uint64_t)o != off) return MZ_ERR_UNSUPPORTED;
|
||||
return fseeko(fp, o, SEEK_SET) == 0 ? MZ_OK : MZ_ERR_IO;
|
||||
}
|
||||
|
||||
static int read_exact(FILE *fp, void *buf, size_t len)
|
||||
{
|
||||
if (fread(buf, 1, len, fp) == len) return MZ_OK;
|
||||
return ferror(fp) ? MZ_ERR_IO : MZ_ERR_TRUNCATED;
|
||||
}
|
||||
|
||||
static int read_at(FILE *fp, uint64_t off, void *buf, size_t len)
|
||||
{
|
||||
int rc = seek_to(fp, off);
|
||||
if (rc != MZ_OK) return rc;
|
||||
return read_exact(fp, buf, len);
|
||||
}
|
||||
|
||||
static int get_file_size(FILE *fp, uint64_t *size)
|
||||
{
|
||||
if (fseeko(fp, 0, SEEK_END) != 0) return MZ_ERR_IO;
|
||||
off_t end = ftello(fp);
|
||||
if (end < 0) return MZ_ERR_IO;
|
||||
*size = (uint64_t)end;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
/* 从文件尾向前找 EOCD 签名;只接受注释长度不越过文件尾的候选。 */
|
||||
static int find_eocd(FILE *fp, uint64_t file_size, uint64_t *eocd_off,
|
||||
uint8_t eocd[MZ_EOCD_MIN])
|
||||
{
|
||||
if (file_size < MZ_EOCD_MIN) return MZ_ERR_TRUNCATED;
|
||||
|
||||
uint64_t tail_len = file_size;
|
||||
if (tail_len > (uint64_t)MZ_EOCD_MIN + MZ_COMMENT_MAX)
|
||||
tail_len = (uint64_t)MZ_EOCD_MIN + MZ_COMMENT_MAX;
|
||||
uint64_t tail_off = file_size - tail_len;
|
||||
|
||||
uint8_t *tail = malloc((size_t)tail_len);
|
||||
if (!tail) return MZ_ERR_MEM;
|
||||
int rc = read_at(fp, tail_off, tail, (size_t)tail_len);
|
||||
if (rc == MZ_OK) {
|
||||
rc = MZ_ERR_FORMAT;
|
||||
for (uint64_t i = tail_len - MZ_EOCD_MIN + 1; i-- > 0; ) {
|
||||
if (rd32(tail + i) != MZ_SIG_EOCD) continue;
|
||||
uint64_t pos = tail_off + i;
|
||||
uint64_t end = 0;
|
||||
if (!u64_add(pos, MZ_EOCD_MIN, &end)) continue;
|
||||
if (!u64_add(end, rd16(tail + i + 20), &end)) continue;
|
||||
if (end > file_size) continue;
|
||||
memcpy(eocd, tail + i, MZ_EOCD_MIN);
|
||||
*eocd_off = pos;
|
||||
rc = MZ_OK;
|
||||
break;
|
||||
}
|
||||
}
|
||||
free(tail);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 解析 central directory;名字先指向 cd 缓冲,成功后在 names_out 里做一份连续副本。 */
|
||||
static int parse_cd(const uint8_t *cd, uint32_t cd_size, int declared,
|
||||
mz_entry *entries, char **names_out)
|
||||
{
|
||||
const uint8_t *p = cd;
|
||||
const uint8_t *end = cd + cd_size;
|
||||
uint64_t names_total = 0;
|
||||
int n = 0;
|
||||
int rc = MZ_OK;
|
||||
|
||||
while (n < declared) {
|
||||
if ((size_t)(end - p) < MZ_CD_MIN || rd32(p) != MZ_SIG_CD) {
|
||||
rc = MZ_ERR_FORMAT;
|
||||
break;
|
||||
}
|
||||
uint16_t flags = rd16(p + 8);
|
||||
uint16_t method = rd16(p + 10);
|
||||
uint32_t crc = rd32(p + 16);
|
||||
uint32_t comp = rd32(p + 20);
|
||||
uint32_t uncomp = rd32(p + 24);
|
||||
uint16_t nlen = rd16(p + 28);
|
||||
uint16_t elen = rd16(p + 30);
|
||||
uint16_t clen = rd16(p + 32);
|
||||
uint16_t disk = rd16(p + 34);
|
||||
uint32_t loff = rd32(p + 42);
|
||||
uint64_t rec = (uint64_t)MZ_CD_MIN + nlen + elen + clen;
|
||||
|
||||
if (rec > (uint64_t)(end - p)) {
|
||||
rc = MZ_ERR_FORMAT;
|
||||
break;
|
||||
}
|
||||
if (disk != 0) {
|
||||
rc = MZ_ERR_UNSUPPORTED;
|
||||
break;
|
||||
}
|
||||
if (comp == 0xFFFFFFFFu || uncomp == 0xFFFFFFFFu || loff == 0xFFFFFFFFu) {
|
||||
rc = MZ_ERR_UNSUPPORTED;
|
||||
break;
|
||||
}
|
||||
|
||||
mz_entry *e = &entries[n];
|
||||
e->name = (const char *)(p + MZ_CD_MIN);
|
||||
e->name_len = nlen;
|
||||
e->method = method;
|
||||
e->flags = flags;
|
||||
e->crc = crc;
|
||||
e->comp_size = comp;
|
||||
e->uncomp_size = uncomp;
|
||||
e->local_offset = loff;
|
||||
|
||||
names_total += (uint64_t)nlen + 1;
|
||||
n++;
|
||||
p += (size_t)rec;
|
||||
}
|
||||
|
||||
if (rc == MZ_OK) {
|
||||
if (n != declared) {
|
||||
rc = MZ_ERR_FORMAT;
|
||||
} else if (names_total > (uint64_t)SIZE_MAX) {
|
||||
rc = MZ_ERR_MEM;
|
||||
} else {
|
||||
char *arena = malloc((size_t)names_total);
|
||||
if (!arena) {
|
||||
rc = MZ_ERR_MEM;
|
||||
} else {
|
||||
char *w = arena;
|
||||
for (int i = 0; i < n; i++) {
|
||||
memcpy(w, entries[i].name, entries[i].name_len);
|
||||
w[entries[i].name_len] = '\0';
|
||||
entries[i].name = w;
|
||||
w += entries[i].name_len + 1;
|
||||
}
|
||||
*names_out = arena;
|
||||
}
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int load_cd(mz_archive *z, uint64_t cd_off, uint32_t cd_size, int declared)
|
||||
{
|
||||
uint8_t *cd = malloc(cd_size);
|
||||
if (!cd) return MZ_ERR_MEM;
|
||||
z->entries = calloc((size_t)declared, sizeof *z->entries);
|
||||
if (!z->entries) {
|
||||
free(cd);
|
||||
return MZ_ERR_MEM;
|
||||
}
|
||||
|
||||
int rc = read_at(z->fp, cd_off, cd, cd_size);
|
||||
if (rc == MZ_OK) rc = parse_cd(cd, cd_size, declared, z->entries, &z->names);
|
||||
if (rc == MZ_OK) {
|
||||
z->count = declared;
|
||||
for (int i = 0; i < declared; i++) {
|
||||
uint64_t end = 0;
|
||||
if (!u64_add(z->entries[i].local_offset, MZ_LOCAL_MIN, &end) ||
|
||||
end > z->file_size) {
|
||||
rc = MZ_ERR_TRUNCATED;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
free(cd);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int mz_open(const char *path, mz_archive **out)
|
||||
{
|
||||
if (!out) return MZ_ERR_ARG;
|
||||
*out = NULL;
|
||||
if (!path) return MZ_ERR_ARG;
|
||||
|
||||
mz_archive *z = calloc(1, sizeof *z);
|
||||
if (!z) return MZ_ERR_MEM;
|
||||
z->fp = fopen(path, "rb");
|
||||
if (!z->fp) {
|
||||
free(z);
|
||||
return MZ_ERR_IO;
|
||||
}
|
||||
|
||||
int rc = get_file_size(z->fp, &z->file_size);
|
||||
uint8_t eocd[MZ_EOCD_MIN];
|
||||
uint64_t eocd_off = 0;
|
||||
uint16_t n_total = 0;
|
||||
uint32_t cd_size = 0;
|
||||
uint32_t cd_off = 0;
|
||||
|
||||
if (rc == MZ_OK) rc = find_eocd(z->fp, z->file_size, &eocd_off, eocd);
|
||||
if (rc == MZ_OK) {
|
||||
uint16_t disk_no = rd16(eocd + 4);
|
||||
uint16_t cd_disk = rd16(eocd + 6);
|
||||
uint16_t n_disk = rd16(eocd + 8);
|
||||
n_total = rd16(eocd + 10);
|
||||
cd_size = rd32(eocd + 12);
|
||||
cd_off = rd32(eocd + 16);
|
||||
|
||||
if (disk_no != 0 || cd_disk != 0) {
|
||||
rc = MZ_ERR_UNSUPPORTED;
|
||||
} else if (n_total == 0xFFFFu || n_disk == 0xFFFFu ||
|
||||
cd_size == 0xFFFFFFFFu || cd_off == 0xFFFFFFFFu) {
|
||||
rc = MZ_ERR_UNSUPPORTED; /* zip64:EOCD 字段饱和 */
|
||||
} else if (n_disk != n_total) {
|
||||
rc = MZ_ERR_FORMAT;
|
||||
} else {
|
||||
uint64_t cd_end = 0;
|
||||
if (!u64_add(cd_off, cd_size, &cd_end) || cd_end > eocd_off) {
|
||||
rc = MZ_ERR_TRUNCATED;
|
||||
} else if (n_total > 0 && (uint64_t)cd_size < (uint64_t)n_total * MZ_CD_MIN) {
|
||||
rc = MZ_ERR_FORMAT;
|
||||
} else if ((uint64_t)cd_size > (uint64_t)SIZE_MAX) {
|
||||
rc = MZ_ERR_UNSUPPORTED;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rc == MZ_OK && n_total > 0) rc = load_cd(z, cd_off, cd_size, n_total);
|
||||
|
||||
if (rc != MZ_OK) {
|
||||
free(z->entries);
|
||||
free(z->names);
|
||||
fclose(z->fp);
|
||||
free(z);
|
||||
return rc;
|
||||
}
|
||||
*out = z;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
void mz_close(mz_archive *z)
|
||||
{
|
||||
if (!z) return;
|
||||
if (z->fp) fclose(z->fp);
|
||||
free(z->entries);
|
||||
free(z->names);
|
||||
free(z);
|
||||
}
|
||||
|
||||
int mz_count(const mz_archive *z)
|
||||
{
|
||||
return z ? z->count : 0;
|
||||
}
|
||||
|
||||
const mz_entry *mz_entry_at(const mz_archive *z, int index)
|
||||
{
|
||||
if (!z || index < 0 || index >= z->count) return NULL;
|
||||
return &z->entries[index];
|
||||
}
|
||||
|
||||
const mz_entry *mz_find(const mz_archive *z, const char *name)
|
||||
{
|
||||
if (!z || !name) return NULL;
|
||||
for (int i = 0; i < z->count; i++) {
|
||||
if (strcmp(z->entries[i].name, name) == 0) return &z->entries[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int mz_entry_is_dir(const mz_entry *e)
|
||||
{
|
||||
return e && e->name_len > 0 && e->name[e->name_len - 1] == '/';
|
||||
}
|
||||
|
||||
static uint32_t crc_buf(const uint8_t *p, size_t n)
|
||||
{
|
||||
uLong c = crc32(0L, Z_NULL, 0);
|
||||
while (n > 0) {
|
||||
uInt chunk = n > MZ_CRC_CHUNK ? MZ_CRC_CHUNK : (uInt)n;
|
||||
c = crc32(c, p, chunk);
|
||||
p += chunk;
|
||||
n -= chunk;
|
||||
}
|
||||
return (uint32_t)c;
|
||||
}
|
||||
|
||||
/* 从 data_off 流式 inflate comp_size 字节到 buf(容量 buf_cap = 声明解压尺寸)。
|
||||
* 输出多于声明尺寸或流提前结束都算 FORMAT/TRUNCATED,不整块读取压缩数据。
|
||||
* prefix=1(M32):装满 buf_cap 就停——只取前缀,不把整条目解压完;此时不判 CRC
|
||||
* (调用方没读完整条目,见 mz_read_prefix)。prefix=0 的语义与旧实现逐字一致。 */
|
||||
static int inflate_entry(FILE *fp, uint64_t data_off, uint64_t comp_size,
|
||||
uint8_t *buf, size_t buf_cap, size_t *out_len, int prefix)
|
||||
{
|
||||
z_stream z;
|
||||
memset(&z, 0, sizeof z);
|
||||
if (inflateInit2(&z, -MAX_WBITS) != Z_OK) return MZ_ERR_MEM;
|
||||
|
||||
/* prefix 模式最多只要 buf_cap 字节(4KB 文件头),输入侧用 4KB 栈缓冲:
|
||||
* 不为一次前缀读分配 64KB(500 页排版 = 500 次 64KB 分配/释放)。 */
|
||||
uint8_t in_stack[MZ_PREFIX_IN_CHUNK];
|
||||
size_t in_chunk = prefix ? sizeof in_stack : MZ_IN_CHUNK;
|
||||
uint8_t *in = prefix ? in_stack : (uint8_t *)malloc(MZ_IN_CHUNK);
|
||||
if (!in) {
|
||||
inflateEnd(&z);
|
||||
return MZ_ERR_MEM;
|
||||
}
|
||||
|
||||
int rc = seek_to(fp, data_off);
|
||||
size_t produced = 0;
|
||||
uint64_t consumed = 0;
|
||||
uint8_t scratch = 0;
|
||||
int stalls = 0;
|
||||
|
||||
while (rc == MZ_OK) {
|
||||
if (prefix && produced == buf_cap) break; /* 前缀够了:不再解压 */
|
||||
if (z.avail_in == 0) {
|
||||
if (consumed >= comp_size) {
|
||||
rc = MZ_ERR_TRUNCATED;
|
||||
break;
|
||||
}
|
||||
uint64_t remain = comp_size - consumed;
|
||||
size_t want = remain > in_chunk ? in_chunk : (size_t)remain;
|
||||
rc = read_exact(fp, in, want);
|
||||
if (rc != MZ_OK) break;
|
||||
z.next_in = in;
|
||||
z.avail_in = (uInt)want;
|
||||
consumed += want;
|
||||
}
|
||||
|
||||
uInt avail;
|
||||
if (produced < buf_cap) {
|
||||
size_t room = buf_cap - produced;
|
||||
z.next_out = buf + produced;
|
||||
avail = room > UINT_MAX ? UINT_MAX : (uInt)room;
|
||||
} else {
|
||||
z.next_out = &scratch; /* 声明尺寸之外的输出 → 直接判坏流 */
|
||||
avail = 1;
|
||||
}
|
||||
z.avail_out = avail;
|
||||
|
||||
uInt in_before = z.avail_in;
|
||||
int zr = inflate(&z, Z_NO_FLUSH);
|
||||
size_t made = (size_t)avail - (size_t)z.avail_out;
|
||||
|
||||
if (made > buf_cap - produced) {
|
||||
rc = MZ_ERR_FORMAT;
|
||||
break;
|
||||
}
|
||||
produced += made;
|
||||
|
||||
if (zr == Z_STREAM_END) break;
|
||||
if (zr == Z_OK) {
|
||||
if (made == 0 && z.avail_in == in_before) {
|
||||
if (++stalls > 8 || (consumed >= comp_size && z.avail_in == 0)) {
|
||||
rc = MZ_ERR_TRUNCATED;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
stalls = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (zr == Z_BUF_ERROR && z.avail_in == 0 && consumed >= comp_size) {
|
||||
rc = MZ_ERR_TRUNCATED;
|
||||
break;
|
||||
}
|
||||
rc = MZ_ERR_FORMAT;
|
||||
break;
|
||||
}
|
||||
|
||||
inflateEnd(&z);
|
||||
if (!prefix) free(in);
|
||||
if (rc == MZ_OK) *out_len = produced;
|
||||
return rc;
|
||||
}
|
||||
|
||||
int mz_read(mz_archive *z, const mz_entry *e, void *buf, size_t buf_size,
|
||||
size_t *out_size)
|
||||
{
|
||||
if (out_size) *out_size = 0;
|
||||
if (!z || !z->fp || !e) return MZ_ERR_ARG;
|
||||
if (!buf && buf_size > 0) return MZ_ERR_ARG;
|
||||
if (e->flags & 1u) return MZ_ERR_UNSUPPORTED;
|
||||
if (e->method != 0 && e->method != 8) return MZ_ERR_UNSUPPORTED;
|
||||
if (e->uncomp_size > (uint64_t)SIZE_MAX) return MZ_ERR_UNSUPPORTED;
|
||||
|
||||
size_t need = (size_t)e->uncomp_size;
|
||||
if (need > buf_size) return MZ_ERR_BUF;
|
||||
|
||||
uint8_t lh[MZ_LOCAL_MIN];
|
||||
int rc = read_at(z->fp, e->local_offset, lh, sizeof lh);
|
||||
if (rc != MZ_OK) return rc;
|
||||
if (rd32(lh) != MZ_SIG_LOCAL) return MZ_ERR_FORMAT;
|
||||
if (rd16(lh + 8) != e->method) return MZ_ERR_FORMAT;
|
||||
|
||||
uint64_t data_off = 0;
|
||||
uint64_t data_end = 0;
|
||||
if (!u64_add(e->local_offset, MZ_LOCAL_MIN, &data_off) ||
|
||||
!u64_add(data_off, rd16(lh + 26), &data_off) ||
|
||||
!u64_add(data_off, rd16(lh + 28), &data_off) ||
|
||||
!u64_add(data_off, e->comp_size, &data_end) ||
|
||||
data_end > z->file_size) {
|
||||
return MZ_ERR_TRUNCATED;
|
||||
}
|
||||
|
||||
if (e->method == 0) {
|
||||
if (e->comp_size != e->uncomp_size) return MZ_ERR_FORMAT;
|
||||
if (need > 0) {
|
||||
rc = read_at(z->fp, data_off, buf, need);
|
||||
if (rc != MZ_OK) return rc;
|
||||
}
|
||||
if (crc_buf((const uint8_t *)buf, need) != e->crc) return MZ_ERR_CRC;
|
||||
} else {
|
||||
size_t made = 0;
|
||||
rc = inflate_entry(z->fp, data_off, e->comp_size, (uint8_t *)buf, need, &made, 0);
|
||||
if (rc != MZ_OK) return rc;
|
||||
if (made != need) return MZ_ERR_FORMAT;
|
||||
if (crc_buf((const uint8_t *)buf, made) != e->crc) return MZ_ERR_CRC;
|
||||
}
|
||||
|
||||
if (out_size) *out_size = need;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int mz_read_alloc(mz_archive *z, const mz_entry *e, void **out_buf,
|
||||
size_t *out_size)
|
||||
{
|
||||
if (!out_buf || !out_size) return MZ_ERR_ARG;
|
||||
*out_buf = NULL;
|
||||
*out_size = 0;
|
||||
if (!z || !e) return MZ_ERR_ARG;
|
||||
if (e->uncomp_size > (uint64_t)SIZE_MAX) return MZ_ERR_UNSUPPORTED;
|
||||
|
||||
size_t need = (size_t)e->uncomp_size;
|
||||
uint8_t *p = malloc(need ? need : 1);
|
||||
if (!p) return MZ_ERR_MEM;
|
||||
|
||||
size_t n = 0;
|
||||
int rc = mz_read(z, e, p, need, &n);
|
||||
if (rc != MZ_OK) {
|
||||
free(p);
|
||||
return rc;
|
||||
}
|
||||
*out_buf = p;
|
||||
*out_size = n;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int mz_read_prefix(mz_archive *z, const mz_entry *e, void *buf, size_t buf_size,
|
||||
size_t *out_size)
|
||||
{
|
||||
if (out_size) *out_size = 0;
|
||||
if (!z || !z->fp || !e) return MZ_ERR_ARG;
|
||||
if (!buf && buf_size > 0) return MZ_ERR_ARG;
|
||||
if (buf_size == 0) return MZ_OK;
|
||||
if (e->flags & 1u) return MZ_ERR_UNSUPPORTED;
|
||||
if (e->method != 0 && e->method != 8) return MZ_ERR_UNSUPPORTED;
|
||||
if (e->uncomp_size > (uint64_t)SIZE_MAX) return MZ_ERR_UNSUPPORTED;
|
||||
|
||||
/* 与 mz_read 同一套局部头/数据范围校验(结构非法一律拒绝)。 */
|
||||
uint8_t lh[MZ_LOCAL_MIN];
|
||||
int rc = read_at(z->fp, e->local_offset, lh, sizeof lh);
|
||||
if (rc != MZ_OK) return rc;
|
||||
if (rd32(lh) != MZ_SIG_LOCAL) return MZ_ERR_FORMAT;
|
||||
if (rd16(lh + 8) != e->method) return MZ_ERR_FORMAT;
|
||||
|
||||
uint64_t data_off = 0;
|
||||
uint64_t data_end = 0;
|
||||
if (!u64_add(e->local_offset, MZ_LOCAL_MIN, &data_off) ||
|
||||
!u64_add(data_off, rd16(lh + 26), &data_off) ||
|
||||
!u64_add(data_off, rd16(lh + 28), &data_off) ||
|
||||
!u64_add(data_off, e->comp_size, &data_end) ||
|
||||
data_end > z->file_size) {
|
||||
return MZ_ERR_TRUNCATED;
|
||||
}
|
||||
|
||||
size_t want = buf_size;
|
||||
if (e->uncomp_size < (uint64_t)want) want = (size_t)e->uncomp_size;
|
||||
|
||||
if (e->method == 0) {
|
||||
if (e->comp_size != e->uncomp_size) return MZ_ERR_FORMAT;
|
||||
if (want > 0) {
|
||||
rc = read_at(z->fp, data_off, buf, want);
|
||||
if (rc != MZ_OK) return rc;
|
||||
}
|
||||
} else {
|
||||
size_t made = 0;
|
||||
rc = inflate_entry(z->fp, data_off, e->comp_size, (uint8_t *)buf, want, &made, 1);
|
||||
if (rc != MZ_OK) return rc;
|
||||
if (made != want) return MZ_ERR_FORMAT; /* 声明尺寸内提前结束 = 坏流 */
|
||||
}
|
||||
|
||||
if (out_size) *out_size = want;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
const char *mz_strerror(int err)
|
||||
{
|
||||
switch (err) {
|
||||
case MZ_OK: return "ok";
|
||||
case MZ_ERR_IO: return "I/O error";
|
||||
case MZ_ERR_FORMAT: return "malformed archive";
|
||||
case MZ_ERR_TRUNCATED: return "archive truncated or offset out of bounds";
|
||||
case MZ_ERR_UNSUPPORTED: return "unsupported zip feature";
|
||||
case MZ_ERR_BUF: return "output buffer too small";
|
||||
case MZ_ERR_MEM: return "out of memory";
|
||||
case MZ_ERR_ARG: return "invalid argument";
|
||||
case MZ_ERR_CRC: return "CRC mismatch";
|
||||
default: return "unknown error";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/* zip.h — 纯 C zip 容器读取器(只依赖 libz 的 inflate/crc32 + stdio)。
|
||||
*
|
||||
* 定位:tuku 漫画阅读器的 CBZ/zip 容器层——开包、列目录、按需解压单条目。
|
||||
* 不依赖 meow_ui.h / SDK / tuku 其它逻辑,宿主可裸测(见 tuku/test/test_zip.c)。
|
||||
*
|
||||
* 用法:
|
||||
* mz_archive *z = NULL;
|
||||
* int rc = mz_open("book.cbz", &z);
|
||||
* if (rc != MZ_OK) { fprintf(stderr, "%s\n", mz_strerror(rc)); return; }
|
||||
* for (int i = 0; i < mz_count(z); i++) {
|
||||
* const mz_entry *e = mz_entry_at(z, i);
|
||||
* if (mz_entry_is_dir(e) || !is_image_name(e->name)) continue;
|
||||
* size_t n = 0;
|
||||
* void *px = NULL;
|
||||
* if (mz_read_alloc(z, e, &px, &n) == MZ_OK) { decode(px, n); free(px); }
|
||||
* }
|
||||
* mz_close(z);
|
||||
*
|
||||
* 线程约束:单个 mz_archive 不是线程安全的(共享 FILE* 位置与条目表只读);
|
||||
* 多线程请各自 mz_open 自己的 archive,或由调用方加锁。模块内无全局可变状态。
|
||||
*
|
||||
* 内存:mz_open 只把 central directory 的条目元数据(条目表 + 名字副本)读进内存,
|
||||
* 不展开条目数据。mz_read 解压到调用方缓冲(缓冲需 >= 该条目 uncomp_size);
|
||||
* mz_read_alloc 只为单个条目分配一次。
|
||||
*
|
||||
* 安全:所有偏移/长度都按文件实际大小校验;越界、截断、坏 magic、坏 CRC 一律
|
||||
* 返回错误码而不崩溃。条目名按原始字节透传(通常是 UTF-8,不做规范化/解码)。
|
||||
* 名字可能是 ".."、绝对路径或反斜杠——调用方若把它落到文件系统,必须自行拒绝
|
||||
* 越界路径。加密、zip64、分卷压缩包明确返回 MZ_ERR_UNSUPPORTED。
|
||||
*
|
||||
* 错误码均为负值;MZ_OK 为 0。mz_strerror 对所有已知码返回静态字符串。
|
||||
*/
|
||||
#ifndef TUKU_LOGIC_ZIP_ZIP_H
|
||||
#define TUKU_LOGIC_ZIP_ZIP_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum {
|
||||
MZ_OK = 0,
|
||||
MZ_ERR_IO = -1, /* 文件读写失败(打开/seek/read) */
|
||||
MZ_ERR_FORMAT = -2, /* 结构非法:坏 magic、central directory 不一致、流损坏 */
|
||||
MZ_ERR_TRUNCATED = -3, /* 声明的偏移/长度越过文件尾(含局部头越界) */
|
||||
MZ_ERR_UNSUPPORTED = -4, /* 加密 / zip64 / 分卷 / 非 stored-deflate 压缩方式 */
|
||||
MZ_ERR_BUF = -5, /* 调用方缓冲小于条目 uncomp_size */
|
||||
MZ_ERR_MEM = -6, /* 内存分配失败 */
|
||||
MZ_ERR_ARG = -7, /* 空指针 / 越界 index 等调用错误 */
|
||||
MZ_ERR_CRC = -8 /* 解压结果 CRC-32 与 central directory 不符 */
|
||||
};
|
||||
|
||||
typedef struct mz_entry {
|
||||
const char *name; /* NUL 结尾;内容为 zip 内原始字节,生命周期同 archive */
|
||||
uint32_t name_len; /* 原始名字字节数(不含结尾 NUL,name 中可能有内嵌 NUL) */
|
||||
uint16_t method; /* 0=stored, 8=deflate;其它值在 mz_read 时返回 UNSUPPORTED */
|
||||
uint16_t flags; /* zip general purpose flags(bit0=加密,bit11=UTF-8 名) */
|
||||
uint32_t crc; /* 条目内容 CRC-32 */
|
||||
uint64_t comp_size; /* 压缩后字节数(archive 内) */
|
||||
uint64_t uncomp_size; /* 原始字节数 */
|
||||
uint64_t local_offset; /* local file header 在 archive 内的偏移 */
|
||||
} mz_entry;
|
||||
|
||||
typedef struct mz_archive mz_archive;
|
||||
|
||||
/* 打开 zip。成功时 *out 为新 archive(mz_close 释放);失败时 *out == NULL。 */
|
||||
int mz_open(const char *path, mz_archive **out);
|
||||
void mz_close(mz_archive *z);
|
||||
|
||||
int mz_count(const mz_archive *z);
|
||||
const mz_entry *mz_entry_at(const mz_archive *z, int index); /* 越界返回 NULL */
|
||||
const mz_entry *mz_find(const mz_archive *z, const char *name); /* 精确 strcmp,找不到 NULL */
|
||||
|
||||
/* 目录条目(名字以 '/' 结尾)。注意 zip 常见但没有强制要求目录条目存在。 */
|
||||
int mz_entry_is_dir(const mz_entry *e);
|
||||
|
||||
/* 解压单条目到 buf。buf_size 必须 >= e->uncomp_size,否则 MZ_ERR_BUF。
|
||||
* 成功时 *out_size = uncomp_size(可为 0)。out_size 可为 NULL。
|
||||
* 校验 CRC 与声明尺寸;失败时 buf 内容不可用。 */
|
||||
int mz_read(mz_archive *z, const mz_entry *e, void *buf, size_t buf_size, size_t *out_size);
|
||||
|
||||
/* mz_read 的便捷版:malloc 恰好 uncomp_size 字节(0 长度条目分配 1 字节)。
|
||||
* 成功时 *out_buf 由调用方 free。 */
|
||||
int mz_read_alloc(mz_archive *z, const mz_entry *e, void **out_buf, size_t *out_size);
|
||||
|
||||
/* 只解压条目**前缀**到 buf(buf_size = 容量):stored 直接读前 buf_size 字节,
|
||||
* deflate 在 inflate 出 buf_size 字节后即停(不整条目解压)。
|
||||
* *out_size = 实际写入字节数(= min(buf_size, uncomp_size))。
|
||||
* 用途:CBZ 排版只看图片头取宽高(PNG IHDR 33 字节 / JPEG SOF / BMP 头都在前 4KB)。
|
||||
* 注意:没有读完整条目,因此**不校验整段 CRC**(只校验结构/长度),不要用它取内容。
|
||||
* 出错语义与 mz_read 相同(越界/坏流/加密/zip64 一律返回错误码)。 */
|
||||
int mz_read_prefix(mz_archive *z, const mz_entry *e, void *buf, size_t buf_size,
|
||||
size_t *out_size);
|
||||
|
||||
const char *mz_strerror(int err);
|
||||
|
||||
#endif /* TUKU_LOGIC_ZIP_ZIP_H */
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# pack_tuku.sh — 一键出包 os.meow.picbrowser.mpk(根布局 tar.gz,可选内联签名)。
|
||||
#
|
||||
# 布局:mpk 内的条目直接是 app.json + picbrowser(无 <pkg>/ 前缀、无 ./ 前缀),
|
||||
# 与安装端「.mpk 内容在 tar 根」的解包口径一致。
|
||||
#
|
||||
# 一键签名(推荐;os.meow.* 无签名设备会拒装):
|
||||
# MEOW_SIGN_KEY=/path/to/signing.key ./pack_tuku.sh
|
||||
#
|
||||
# 环境变量:
|
||||
# MEOW_SIGN 指定 meow_sign 可执行文件;未设时依次找 PATH 里的 meow_sign、
|
||||
# 再回退 $HOME/Proj/MeowOS-For-R3P2/patch_v2/meow_sign/meow_sign。
|
||||
# MEOW_SIGN_KEY 私钥路径。未设 → 警告并产出未签名包(退出码 0);
|
||||
# 设了但找不到工具/密钥或签名失败 → 报错退出(不静默交付未签名包)。
|
||||
# 若同目录存在 <key>.pub,签名后自动 verify。
|
||||
#
|
||||
# 注意:签名是往 tar.gz 尾部追加 footer。签名后的 mpk **不要**再跑 tar/gzip 类命令
|
||||
# (gzip 会报 "trailing garbage ignored",属预期,不是包损坏)。因此根布局断言
|
||||
# 必须在签名前用 tar -tzf 做。
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
PKG=os.meow.picbrowser
|
||||
MPK="$PKG.mpk"
|
||||
STAGE="build/$PKG"
|
||||
|
||||
[ -x picbrowser ] || ./build_tuku.sh
|
||||
|
||||
rm -rf "$STAGE"
|
||||
mkdir -p "$STAGE"
|
||||
cp app.json picbrowser "$STAGE/"
|
||||
|
||||
rm -f "$MPK"
|
||||
tar -C "$STAGE" -czf "$MPK" app.json picbrowser
|
||||
|
||||
# 根布局断言(签名前):第一层有 app.json,且不带 <pkg>/ 或 ./ 前缀。
|
||||
listing="$(tar -C "$STAGE" -tzf "$MPK")"
|
||||
printf '%s\n' "$listing"
|
||||
if ! printf '%s\n' "$listing" | grep -qx 'app.json'; then
|
||||
echo "错误:$MPK 根层缺少 app.json(布局不是根布局)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if printf '%s\n' "$listing" | grep -q "^\(\./\)\?$PKG/"; then
|
||||
echo "错误:$MPK 仍带 $PKG/ 前缀(嵌套布局)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find_sign_tool() {
|
||||
if [ -n "${MEOW_SIGN:-}" ]; then
|
||||
printf '%s\n' "$MEOW_SIGN"
|
||||
elif command -v meow_sign >/dev/null 2>&1; then
|
||||
command -v meow_sign
|
||||
elif [ -x "$HOME/Proj/MeowOS-For-R3P2/patch_v2/meow_sign/meow_sign" ]; then
|
||||
printf '%s\n' "$HOME/Proj/MeowOS-For-R3P2/patch_v2/meow_sign/meow_sign"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -z "${MEOW_SIGN_KEY:-}" ]; then
|
||||
echo "警告:未签名(未设置 MEOW_SIGN_KEY),已产出未签名包 $MPK" >&2
|
||||
echo " 设 MEOW_SIGN_KEY=<私钥> 后重跑,或手动 sign;os.meow.* 未签名设备拒装。" >&2
|
||||
else
|
||||
if [ ! -f "$MEOW_SIGN_KEY" ]; then
|
||||
echo "错误:MEOW_SIGN_KEY=$MEOW_SIGN_KEY 不存在" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! SIGN_TOOL="$(find_sign_tool)"; then
|
||||
echo "错误:找不到 meow_sign(可用 MEOW_SIGN=/path/to/meow_sign 指定);$MPK 未签名" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -x "$SIGN_TOOL" ]; then
|
||||
echo "错误:$SIGN_TOOL 不可执行(可用 MEOW_SIGN 指定可执行的 meow_sign);$MPK 未签名" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! "$SIGN_TOOL" sign "$MPK" "$MEOW_SIGN_KEY"; then
|
||||
echo "错误:meow_sign sign 失败,$MPK 仍是未签名包" >&2
|
||||
echo " 若提示 cannot execute / required file not found,多半是该二进制与本机不兼容,用 MEOW_SIGN 指定可执行版本。" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -f "$MEOW_SIGN_KEY.pub" ]; then
|
||||
if ! "$SIGN_TOOL" verify "$MPK" "$MEOW_SIGN_KEY.pub"; then
|
||||
echo "错误:meow_sign verify 失败,$MPK 签名无效" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "提示:未找到 $MEOW_SIGN_KEY.pub,跳过 verify"
|
||||
fi
|
||||
# 签名后不要再 tar -t/-z:footer 在 gzip 流外,gzip 会报 trailing garbage(预期)。
|
||||
fi
|
||||
|
||||
ls -la "$MPK"
|
||||
Executable
+240
@@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成 tuku 档案册(CBZ)集成测试夹具:真实 zipfile 产物 + 定点字节修补。
|
||||
|
||||
用法: python3 make_cbz_samples.py <out_dir>
|
||||
|
||||
产出:
|
||||
<out>/library/ 书库夹具(文件夹册 + 档案册混排)
|
||||
bookA/001.png 002.png 文件夹册(2 页,PNG)
|
||||
vol2.cbz 3 页:PNG(41x31) / PNG(40x30) / 中文名 PNG(12x34)
|
||||
+ 非图片、目录项、0 字节条目(都应被过滤)
|
||||
Vol10.cbz 1 页 BMP(7x9),stored
|
||||
box/box1.cbz box2.cbz 只含档案册的子目录(书库行应写 "2 books")
|
||||
notabook.zip 只有文本 → 不是书
|
||||
emptypages.cbz 目录项 + 文本 → 不是书
|
||||
broken.cbz 非 zip 字节 → 不是书
|
||||
truncated.cbz vol2 砍半(EOCD 丢失) → 不是书
|
||||
.hidden.cbz 隐藏名 → 不参与
|
||||
<out>/features/
|
||||
pathsafe.cbz 安全/不安全条目混排(只应收 2 个安全图片页)
|
||||
cjk.cbz 中文名自然序(第1页 < 第2页 < 第10页)
|
||||
big.cbz 80 页 8x8 PNG(解压缓存 LRU 上限用)
|
||||
bomb.cbz CD 声明单页 96MiB(应被过滤,防 zip 炸弹)
|
||||
<out>/manifest.txt 每档案每条目一行:<档案相对路径>\t<条目名>\t<crc32>\t<size>
|
||||
|
||||
图片内容都是结构合法的 PNG/BMP/JPEG 头(PNG 是真图),C 测试用假解码器,
|
||||
只校验解压字节与 manifest 的 CRC/尺寸一致、以及 meta 探测出的原始尺寸。
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import zipfile
|
||||
import zlib
|
||||
|
||||
DATE = (2024, 1, 1, 0, 0, 0)
|
||||
HUGE_PAGE_LEN = 33 * 1024 * 1024 # > 32MiB 预算:锁定「单条目一次性溢出」行为
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 图片构造
|
||||
def png_bytes(w, h, seed=0):
|
||||
raw = bytearray()
|
||||
for y in range(h):
|
||||
raw.append(0) # filter: none
|
||||
for x in range(w):
|
||||
raw += bytes(((x * 7 + y * 13 + seed) & 0xFF,
|
||||
(x * 3 + y * 5 + seed) & 0xFF,
|
||||
(x + y + seed) & 0xFF))
|
||||
|
||||
def chunk(tag, data):
|
||||
return (struct.pack(">I", len(data)) + tag + data +
|
||||
struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))
|
||||
|
||||
ihdr = struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0) # 8-bit RGB
|
||||
return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) +
|
||||
chunk(b"IDAT", zlib.compress(bytes(raw), 9)) + chunk(b"IEND", b""))
|
||||
|
||||
|
||||
def bmp_bytes(w, h):
|
||||
stride = (w * 3 + 3) & ~3
|
||||
px = bytes(((i * 11) & 0xFF) for i in range(stride * h))
|
||||
hdr = b"BM" + struct.pack("<IHHI", 14 + 40 + len(px), 0, 0, 54)
|
||||
ih = struct.pack("<IiiHHIIiiII", 40, w, h, 1, 24, 0, len(px), 2835, 2835, 0, 0)
|
||||
return hdr + ih + px
|
||||
|
||||
|
||||
def jpeg_header_bytes(w, h):
|
||||
"""SOI + APP0 + DQT + SOF0 + EOI:头段合法(探测只看 SOF0),不做熵编码。"""
|
||||
app0 = (b"\xFF\xE0" + struct.pack(">H", 16) +
|
||||
b"JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00")
|
||||
dqt = b"\xFF\xDB" + struct.pack(">H", 67) + b"\x00" + bytes(64)
|
||||
sof = b"\xFF\xC0" + struct.pack(">HBHHB", 11, 8, h, w, 1) + b"\x01\x11\x00"
|
||||
return b"\xFF\xD8" + app0 + dqt + sof + b"\xFF\xD9"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- zip 构造
|
||||
def write_zip(path, entries):
|
||||
"""entries: [(name, data, zipfile 压缩方式)]"""
|
||||
with zipfile.ZipFile(path, "w") as z:
|
||||
for name, data, method in entries:
|
||||
zi = zipfile.ZipInfo(name, DATE)
|
||||
zi.compress_type = method
|
||||
z.writestr(zi, data)
|
||||
|
||||
|
||||
def find_cd_record(data, name):
|
||||
eocd = data.rfind(b"PK\x05\x06")
|
||||
if eocd < 0:
|
||||
raise SystemExit("generator: no EOCD")
|
||||
total, _cd_size, cd_off = struct.unpack_from("<HII", data, eocd + 10)
|
||||
p = cd_off
|
||||
for _ in range(total):
|
||||
if bytes(data[p:p + 4]) != b"PK\x01\x02":
|
||||
raise SystemExit("generator: bad CD signature")
|
||||
nlen, elen, clen = struct.unpack_from("<HHH", data, p + 28)
|
||||
got = bytes(data[p + 46:p + 46 + nlen]).decode("utf-8")
|
||||
if got == name:
|
||||
return p
|
||||
p += 46 + nlen + elen + clen
|
||||
raise SystemExit("generator: entry not found: " + name)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 夹具
|
||||
def build_library(ro):
|
||||
lib = os.path.join(ro, "library")
|
||||
os.makedirs(lib, exist_ok=True)
|
||||
|
||||
os.makedirs(os.path.join(lib, "bookA"), exist_ok=True)
|
||||
with open(os.path.join(lib, "bookA", "001.png"), "wb") as f:
|
||||
f.write(png_bytes(30, 20, 1))
|
||||
with open(os.path.join(lib, "bookA", "002.png"), "wb") as f:
|
||||
f.write(png_bytes(30, 21, 2))
|
||||
|
||||
write_zip(os.path.join(lib, "vol2.cbz"), [
|
||||
("ch01/p10.png", png_bytes(40, 30, 3), zipfile.ZIP_DEFLATED),
|
||||
("readme.txt", b"not a page\n", zipfile.ZIP_STORED),
|
||||
("ch01/p2.png", png_bytes(41, 31, 4), zipfile.ZIP_DEFLATED),
|
||||
("onlydir/", b"", zipfile.ZIP_STORED),
|
||||
("\u4e2d\u6587\u540d.png", png_bytes(12, 34, 5), zipfile.ZIP_DEFLATED),
|
||||
("empty.bin", b"", zipfile.ZIP_DEFLATED),
|
||||
])
|
||||
|
||||
write_zip(os.path.join(lib, "Vol10.cbz"), [
|
||||
("cover.bmp", bmp_bytes(7, 9), zipfile.ZIP_STORED),
|
||||
])
|
||||
|
||||
os.makedirs(os.path.join(lib, "box"), exist_ok=True)
|
||||
write_zip(os.path.join(lib, "box", "box1.cbz"), [
|
||||
("a.png", png_bytes(8, 8, 6), zipfile.ZIP_DEFLATED),
|
||||
("b.png", png_bytes(8, 9, 7), zipfile.ZIP_DEFLATED),
|
||||
])
|
||||
write_zip(os.path.join(lib, "box", "box2.cbz"), [
|
||||
("c.jpg", jpeg_header_bytes(12, 34), zipfile.ZIP_DEFLATED),
|
||||
])
|
||||
|
||||
write_zip(os.path.join(lib, "notabook.zip"), [
|
||||
("readme.txt", b"nothing to read\n", zipfile.ZIP_STORED),
|
||||
])
|
||||
write_zip(os.path.join(lib, "emptypages.cbz"), [
|
||||
("onlydir/", b"", zipfile.ZIP_STORED),
|
||||
("readme.txt", b"x\n", zipfile.ZIP_STORED),
|
||||
])
|
||||
with open(os.path.join(lib, "broken.cbz"), "wb") as f:
|
||||
f.write(b"NOTAZIP!" * 100)
|
||||
|
||||
raw = open(os.path.join(lib, "vol2.cbz"), "rb").read()
|
||||
with open(os.path.join(lib, "truncated.cbz"), "wb") as f:
|
||||
f.write(raw[:len(raw) // 2])
|
||||
|
||||
write_zip(os.path.join(lib, ".hidden.cbz"), [
|
||||
("h.png", png_bytes(8, 8, 8), zipfile.ZIP_DEFLATED),
|
||||
])
|
||||
|
||||
|
||||
def build_features(ro):
|
||||
feat = os.path.join(ro, "features")
|
||||
os.makedirs(feat, exist_ok=True)
|
||||
|
||||
write_zip(os.path.join(feat, "pathsafe.cbz"), [
|
||||
("ok1.png", png_bytes(10, 10, 9), zipfile.ZIP_DEFLATED),
|
||||
("../evil.png", png_bytes(10, 10, 10), zipfile.ZIP_DEFLATED),
|
||||
("/abs.png", png_bytes(10, 10, 11), zipfile.ZIP_DEFLATED),
|
||||
("a/../../b.png", png_bytes(10, 10, 12), zipfile.ZIP_DEFLATED),
|
||||
("sub/ok2.jpg", jpeg_header_bytes(13, 17), zipfile.ZIP_DEFLATED),
|
||||
("sub/../ok3.png", png_bytes(10, 10, 13), zipfile.ZIP_DEFLATED),
|
||||
(".hidden/x.png", png_bytes(10, 10, 14), zipfile.ZIP_DEFLATED),
|
||||
("._mac.png", png_bytes(10, 10, 15), zipfile.ZIP_DEFLATED),
|
||||
("sub\\win.png", png_bytes(10, 10, 16), zipfile.ZIP_DEFLATED),
|
||||
("text.txt", b"nope\n", zipfile.ZIP_STORED),
|
||||
("dir/", b"", zipfile.ZIP_STORED),
|
||||
])
|
||||
|
||||
write_zip(os.path.join(feat, "cjk.cbz"), [
|
||||
("\u7b2c2\u9875.png", png_bytes(9, 9, 17), zipfile.ZIP_DEFLATED),
|
||||
("\u7b2c10\u9875.png", png_bytes(9, 9, 18), zipfile.ZIP_DEFLATED),
|
||||
("\u7b2c1\u9875.png", png_bytes(9, 9, 19), zipfile.ZIP_DEFLATED),
|
||||
])
|
||||
|
||||
entries = []
|
||||
for i in range(80):
|
||||
entries.append(("p%03d.png" % i, png_bytes(8, 8, 20 + i), zipfile.ZIP_DEFLATED))
|
||||
write_zip(os.path.join(feat, "big.cbz"), entries)
|
||||
|
||||
bomb = os.path.join(feat, "bomb.cbz")
|
||||
write_zip(bomb, [("huge.png", png_bytes(10, 10, 30), zipfile.ZIP_STORED)])
|
||||
data = bytearray(open(bomb, "rb").read())
|
||||
rec = find_cd_record(data, "huge.png")
|
||||
struct.pack_into("<I", data, rec + 24, 0x06000000) # 96MiB > 64MiB 上限
|
||||
with open(bomb, "wb") as f:
|
||||
f.write(bytes(data))
|
||||
|
||||
raw = open(os.path.join(feat, "pathsafe.cbz"), "rb").read()
|
||||
with open(os.path.join(feat, "truncated.cbz"), "wb") as f:
|
||||
f.write(raw[:len(raw) // 2])
|
||||
|
||||
# 单页 33MiB(PNG 头 + 可压零填充,deflate 后只有几十 KB):
|
||||
# 页过滤放行(< 64MiB),解压后落缓存时应允许一次性超过 32MiB 预算。
|
||||
head = png_bytes(64, 64, 31)
|
||||
payload = head + b"\x00" * (HUGE_PAGE_LEN - len(head))
|
||||
write_zip(os.path.join(feat, "hugepage.cbz"), [
|
||||
("huge.png", payload, zipfile.ZIP_DEFLATED),
|
||||
])
|
||||
|
||||
|
||||
def write_manifest(ro):
|
||||
"""每档案每条目:<相对路径>\t<条目名>\t<crc32>\t<size>(Tab 分隔,UTF-8 原名)。"""
|
||||
lines = []
|
||||
for base, _dirs, files in os.walk(ro):
|
||||
for name in sorted(files):
|
||||
if not name.lower().endswith((".cbz", ".zip")):
|
||||
continue
|
||||
if name == "hugepage.cbz":
|
||||
continue # 33MiB 页只为缓存溢出测试,manifest 不校验它
|
||||
path = os.path.join(base, name)
|
||||
rel = os.path.relpath(path, ro)
|
||||
try:
|
||||
with zipfile.ZipFile(path) as z:
|
||||
for info in z.infolist():
|
||||
if info.is_dir():
|
||||
continue
|
||||
payload = z.read(info)
|
||||
lines.append("%s\t%s\t%08x\t%d" % (
|
||||
rel, info.filename, zlib.crc32(payload) & 0xFFFFFFFF,
|
||||
len(payload)))
|
||||
except zipfile.BadZipFile:
|
||||
continue
|
||||
with open(os.path.join(ro, "manifest.txt"), "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit("usage: make_cbz_samples.py <out_dir>")
|
||||
ro = sys.argv[1]
|
||||
os.makedirs(ro, exist_ok=True)
|
||||
build_library(ro)
|
||||
build_features(ro)
|
||||
write_manifest(ro)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""make_mem_samples.py — 为 test_reader_membudget.c 生成解码夹具(M22 / M27)。
|
||||
|
||||
用法:python3 make_mem_samples.py <输出目录>
|
||||
依赖:Pillow。没有 Pillow 时测试自动 SKIP JPEG 相关断言(BMP/PNG 小夹具由 C 直接写)。
|
||||
|
||||
M27 追加「6 类典型页」与「回退路径」夹具,用来断言门控模型与真缩解后端同口径:
|
||||
典型(应全部走后端、不占位):
|
||||
base2000.jpg 2000x3000 baseline
|
||||
prog2000.jpg 2000x3000 progressive
|
||||
restart2000.jpg 2000x3000 progressive + restart markers
|
||||
page1600.png 1600x2400 PNG
|
||||
page1600.bmp 1600x2400 BMP
|
||||
mp4.jpg 2320x1740(4MP)
|
||||
mp12.jpg 4000x3000(12MP)
|
||||
回退(后端拒绝、按旧 stb 门控占位):
|
||||
cmyk.jpg 2000x3000 CMYK(4 分量)
|
||||
adam7.png 1600x2400 Adam7 交错
|
||||
mono1.bmp 1600x2400 1bpp
|
||||
M22 旧断言仍用:
|
||||
page.jpg / big4.jpg / prog.jpg(1000x1500 / 2000x2000 / 1000x1500 progressive)
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
def pattern(w, h, mode="RGB"):
|
||||
im = Image.new(mode, (w, h), (248, 247, 244) if mode == "RGB" else 200)
|
||||
d = ImageDraw.Draw(im)
|
||||
for k in range(0, min(w, h) - 200, 70):
|
||||
d.rectangle([20 + k, 20 + k, 160 + k, 120 + k], outline=(10, 10, 12), width=3)
|
||||
return im
|
||||
|
||||
|
||||
def chunk(tag, data):
|
||||
return (struct.pack(">I", len(data)) + tag + data +
|
||||
struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))
|
||||
|
||||
|
||||
def write_adam7(path, img):
|
||||
"""手写 Adam7 交错 PNG(Pillow 不支持写交错)。RGB8。"""
|
||||
w, h = img.size
|
||||
px = img.load()
|
||||
raw = bytearray()
|
||||
passes = [(0, 0, 8, 8), (4, 0, 8, 8), (0, 4, 4, 8), (2, 0, 4, 4),
|
||||
(0, 2, 2, 4), (1, 0, 2, 2), (0, 1, 1, 2)]
|
||||
for (x0, y0, dx, dy) in passes:
|
||||
xs = list(range(x0, w, dx))
|
||||
ys = list(range(y0, h, dy))
|
||||
if not xs or not ys:
|
||||
continue
|
||||
for y in ys:
|
||||
raw.append(0)
|
||||
for x in xs:
|
||||
r, g, b = px[x, y][:3]
|
||||
raw += bytes((r, g, b))
|
||||
ihdr = struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 1)
|
||||
data = (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) +
|
||||
chunk(b"IDAT", zlib.compress(bytes(raw))) + chunk(b"IEND", b""))
|
||||
with open(path, "wb") as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print(__doc__)
|
||||
return 2
|
||||
out = sys.argv[1]
|
||||
os.makedirs(out, exist_ok=True)
|
||||
|
||||
# ---- M22 旧断言夹具 ----
|
||||
pattern(1000, 1500).save(os.path.join(out, "page.jpg"), "JPEG", quality=60, subsampling=2)
|
||||
pattern(2000, 2000).save(os.path.join(out, "big4.jpg"), "JPEG", quality=60, subsampling=2)
|
||||
pattern(1000, 1500).save(os.path.join(out, "prog.jpg"), "JPEG", quality=60, subsampling=2,
|
||||
progressive=True)
|
||||
|
||||
# ---- M27 6 类典型页 ----
|
||||
big = pattern(2000, 3000)
|
||||
big.save(os.path.join(out, "base2000.jpg"), "JPEG", quality=60, subsampling=2)
|
||||
big.save(os.path.join(out, "prog2000.jpg"), "JPEG", quality=60, subsampling=2, progressive=True)
|
||||
big.save(os.path.join(out, "restart2000.jpg"), "JPEG", quality=60, subsampling=2,
|
||||
progressive=True, restart_marker_blocks=8)
|
||||
pattern(1600, 2400).save(os.path.join(out, "page1600.png"), "PNG")
|
||||
pattern(1600, 2400).save(os.path.join(out, "page1600.bmp"), "BMP")
|
||||
pattern(2320, 1740).save(os.path.join(out, "mp4.jpg"), "JPEG", quality=60, subsampling=2)
|
||||
pattern(4000, 3000).save(os.path.join(out, "mp12.jpg"), "JPEG", quality=60, subsampling=2)
|
||||
|
||||
# ---- M27 回退路径 ----
|
||||
big.convert("CMYK").save(os.path.join(out, "cmyk.jpg"), "JPEG", quality=60)
|
||||
write_adam7(os.path.join(out, "adam7.png"), pattern(1600, 2400))
|
||||
pattern(1600, 2400).convert("1").save(os.path.join(out, "mono1.bmp"), "BMP")
|
||||
|
||||
# ---- M27 r1(P1-1):头部判支持但解码中途失败/后端拒绝的坏文件 ----
|
||||
# 1) 截断的 12MP JPEG:SOF 仍在(probe/矩阵判 backend),熵段被腰斩 → 后端失败;
|
||||
# 旧实现会静默回退 stb 并分配 ~18MB。
|
||||
with open(os.path.join(out, "mp12.jpg"), "rb") as f:
|
||||
blob = f.read(200000)
|
||||
with open(os.path.join(out, "trunc2000.jpg"), "wb") as f:
|
||||
f.write(blob)
|
||||
# 2) 3000x3000 24bpp BMP,只有 54 字节头(无像素):矩阵判 backend,后端失败;
|
||||
# 旧实现回退 stb 会 malloc 36MB 并"解出"一张全黑图。
|
||||
hdr = bytearray(54)
|
||||
hdr[0:2] = b"BM"
|
||||
struct.pack_into("<I", hdr, 2, 54) # 谎报文件大小
|
||||
struct.pack_into("<I", hdr, 10, 54) # 像素数据偏移
|
||||
struct.pack_into("<I", hdr, 14, 40) # BITMAPINFOHEADER
|
||||
struct.pack_into("<i", hdr, 18, 3000) # width
|
||||
struct.pack_into("<i", hdr, 22, 3000) # height
|
||||
struct.pack_into("<H", hdr, 26, 1) # planes
|
||||
struct.pack_into("<H", hdr, 28, 24) # bpp
|
||||
with open(os.path.join(out, "hdr3000.bmp"), "wb") as f:
|
||||
f.write(hdr)
|
||||
# 3) 非法位深 PNG(color=2 RGB + depth=1):后端 png_depth_ok 拒绝 → 必须判 stb;
|
||||
# 旧矩阵只按 color 算位宽 → 误判 backend,失败后回退 stb 分配 ~16MB。
|
||||
w, h = 1600, 2400
|
||||
rowbytes = (w * 3 + 7) // 8 # 600 字节/行(depth=1 RGB)
|
||||
raw = bytearray()
|
||||
for y in range(h):
|
||||
raw.append(0)
|
||||
raw += bytes((x * 7 + y) & 0xFF for x in range(rowbytes))
|
||||
ihdr = struct.pack(">IIBBBBB", w, h, 1, 2, 0, 0, 0)
|
||||
data = (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) +
|
||||
chunk(b"IDAT", zlib.compress(bytes(raw))) + chunk(b"IEND", b""))
|
||||
with open(os.path.join(out, "baddepth.png"), "wb") as f:
|
||||
f.write(data)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""生成 tuku zip 读取器的宿主测试样例(真实 zipfile + 定点字节修补)。
|
||||
|
||||
用法: python3 make_zip_samples.py <out_dir>
|
||||
|
||||
生成:
|
||||
basic.zip 5 条目:stored 文本 / deflate 大二进制 / 中文名 deflate /
|
||||
目录项 / 0 字节 deflate
|
||||
comment.zip 单条目 + 归档注释(EOCD 注释长度路径)
|
||||
empty.zip 0 条目
|
||||
truncated.zip basic.zip 砍半(EOCD 丢失)
|
||||
badmagic.zip 非 zip 字节
|
||||
badoffset.zip 首条目 local header 偏移改成 0xFFFFFF00(越界)
|
||||
badcrc.zip 首条目 CD 里 CRC 翻转(内容解出但校验失败)
|
||||
method99.zip 首条目压缩方式改成 99,CD 与局部头都改
|
||||
badlocal.zip 首条目局部头签名改成 "BAD!"
|
||||
bigsize.zip deflate 条目 CD comp_size 改成 8 MiB(数据越界)
|
||||
zip64claim.zip EOCD 总条目数改成 0xFFFF(zip64 标记)
|
||||
bigrand.zip 单条目 256 KiB 不可压 deflate(压缩流 > 64 KiB,覆盖分块 inflate)
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
PAGE2 = (b"MEOW" * 10 + bytes(range(256))) * 180 # 53280 字节,可压缩
|
||||
HELLO = b"hello zip\n" * 3
|
||||
CJK_NAME = "sub/dir/\u4e2d\u6587\u540d.txt"
|
||||
CJK_DATA = "\u4e2d\u6587\u5185\u5bb9\n".encode("utf-8")
|
||||
BIGRAND_LEN = 262144
|
||||
|
||||
|
||||
def lcg_bytes(n):
|
||||
"""与 test_zip.c 的 build_rand 同一个 LCG,用于 256 KiB 不可压样例。"""
|
||||
x = 123456789
|
||||
out = bytearray(n)
|
||||
for i in range(n):
|
||||
x = (1103515245 * x + 12345) & 0x7FFFFFFF
|
||||
out[i] = (x >> 16) & 0xFF
|
||||
return bytes(out)
|
||||
|
||||
EOCD_SIG = b"PK\x05\x06"
|
||||
CD_SIG = b"PK\x01\x02"
|
||||
|
||||
|
||||
def write_basic(path):
|
||||
with zipfile.ZipFile(path, "w") as z:
|
||||
z.writestr(zipfile.ZipInfo("hello.txt", (2024, 1, 1, 0, 0, 0)),
|
||||
HELLO, compress_type=zipfile.ZIP_STORED)
|
||||
z.writestr(zipfile.ZipInfo("page2.png", (2024, 1, 1, 0, 0, 0)),
|
||||
PAGE2, compress_type=zipfile.ZIP_DEFLATED)
|
||||
z.writestr(zipfile.ZipInfo(CJK_NAME, (2024, 1, 1, 0, 0, 0)),
|
||||
CJK_DATA, compress_type=zipfile.ZIP_DEFLATED)
|
||||
z.writestr(zipfile.ZipInfo("onlydir/", (2024, 1, 1, 0, 0, 0)),
|
||||
b"", compress_type=zipfile.ZIP_STORED)
|
||||
z.writestr(zipfile.ZipInfo("empty.bin", (2024, 1, 1, 0, 0, 0)),
|
||||
b"", compress_type=zipfile.ZIP_DEFLATED)
|
||||
|
||||
|
||||
def write_comment(path):
|
||||
with zipfile.ZipFile(path, "w") as z:
|
||||
z.writestr("note.txt", b"commented\n", compress_type=zipfile.ZIP_DEFLATED)
|
||||
z.comment = b"meow archive comment"
|
||||
|
||||
|
||||
def load(path):
|
||||
with open(path, "rb") as f:
|
||||
return bytearray(f.read())
|
||||
|
||||
|
||||
def save(path, data):
|
||||
with open(path, "wb") as f:
|
||||
f.write(bytes(data))
|
||||
|
||||
|
||||
def eocd_info(data):
|
||||
i = data.rfind(EOCD_SIG)
|
||||
if i < 0:
|
||||
raise SystemExit("generator: no EOCD in fixture")
|
||||
total, cd_size, cd_off = struct.unpack_from("<HII", data, i + 10)
|
||||
return i, total, cd_size, cd_off
|
||||
|
||||
|
||||
def cd_records(data):
|
||||
_, total, _cd_size, cd_off = eocd_info(data)
|
||||
out = []
|
||||
p = cd_off
|
||||
for _ in range(total):
|
||||
if bytes(data[p:p + 4]) != CD_SIG:
|
||||
raise SystemExit("generator: bad CD signature")
|
||||
nlen, elen, clen = struct.unpack_from("<HHH", data, p + 28)
|
||||
loff = struct.unpack_from("<I", data, p + 42)[0]
|
||||
name = bytes(data[p + 46:p + 46 + nlen]).decode("utf-8")
|
||||
out.append({"off": p, "name": name, "local": loff})
|
||||
p += 46 + nlen + elen + clen
|
||||
return out
|
||||
|
||||
|
||||
def find_record(data, name):
|
||||
for r in cd_records(data):
|
||||
if r["name"] == name:
|
||||
return r
|
||||
raise SystemExit("generator: entry not found: " + name)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
raise SystemExit("usage: make_zip_samples.py <out_dir>")
|
||||
out = sys.argv[1]
|
||||
os.makedirs(out, exist_ok=True)
|
||||
|
||||
basic = os.path.join(out, "basic.zip")
|
||||
write_basic(basic)
|
||||
write_comment(os.path.join(out, "comment.zip"))
|
||||
with zipfile.ZipFile(os.path.join(out, "empty.zip"), "w"):
|
||||
pass
|
||||
|
||||
raw = load(basic)
|
||||
save(os.path.join(out, "truncated.zip"), raw[:len(raw) // 2])
|
||||
save(os.path.join(out, "badmagic.zip"), b"NOTAZIP!" * 100)
|
||||
|
||||
d = load(basic)
|
||||
hello = find_record(d, "hello.txt")
|
||||
struct.pack_into("<I", d, hello["off"] + 42, 0xFFFFFF00)
|
||||
save(os.path.join(out, "badoffset.zip"), d)
|
||||
|
||||
d = load(basic)
|
||||
hello = find_record(d, "hello.txt")
|
||||
crc = struct.unpack_from("<I", d, hello["off"] + 16)[0]
|
||||
struct.pack_into("<I", d, hello["off"] + 16, crc ^ 0xFFFFFFFF)
|
||||
save(os.path.join(out, "badcrc.zip"), d)
|
||||
|
||||
d = load(basic)
|
||||
hello = find_record(d, "hello.txt")
|
||||
struct.pack_into("<H", d, hello["off"] + 10, 99)
|
||||
struct.pack_into("<H", d, hello["local"] + 8, 99)
|
||||
save(os.path.join(out, "method99.zip"), d)
|
||||
|
||||
d = load(basic)
|
||||
hello = find_record(d, "hello.txt")
|
||||
d[hello["local"]:hello["local"] + 4] = b"BAD!"
|
||||
save(os.path.join(out, "badlocal.zip"), d)
|
||||
|
||||
d = load(basic)
|
||||
page2 = find_record(d, "page2.png")
|
||||
struct.pack_into("<I", d, page2["off"] + 20, 0x00800000)
|
||||
save(os.path.join(out, "bigsize.zip"), d)
|
||||
|
||||
d = load(basic)
|
||||
eocd, _total, _cd_size, _cd_off = eocd_info(d)
|
||||
struct.pack_into("<H", d, eocd + 10, 0xFFFF)
|
||||
save(os.path.join(out, "zip64claim.zip"), d)
|
||||
|
||||
with zipfile.ZipFile(os.path.join(out, "bigrand.zip"), "w") as z:
|
||||
z.writestr("rand.bin", lcg_bytes(BIGRAND_LEN),
|
||||
compress_type=zipfile.ZIP_DEFLATED)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
# run_cbz_tests.sh — M11 档案册(CBZ/zip)集成宿主测试(纯 C + -lz,无 SDK/设备)。
|
||||
# 夹具由 test/make_cbz_samples.py 现场生成到 $TMPDIR 临时目录,结束后删除。
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
SDK="${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk}"
|
||||
trap 'rm -f test/test_cbz' EXIT
|
||||
gcc -Wall -Wextra -Werror -O1 -I. -I"$SDK" \
|
||||
test/test_cbz.c logic/library.c logic/reader.c \
|
||||
logic/reader/page_source.c logic/reader/archive_source.c \
|
||||
logic/reader/page_cache.c logic/zip/zip.c test/settings_reader_stub.c \
|
||||
-lz -o test/test_cbz
|
||||
./test/test_cbz ./test/make_cbz_samples.py
|
||||
Executable
+180
@@ -0,0 +1,180 @@
|
||||
#!/bin/bash
|
||||
# run_tests.sh — 宿主 gcc 构建并运行图库测试(不链 SDK 静态库、不需要设备)。
|
||||
# - test_library:单层目录扫描(目录+图片+档案册)/自然排序/父目录/根前缀判据/阅读方向
|
||||
# - test_browse_nav:访问放开(系统/空/无图目录可进入)、图片与档案册成行、截断、不可读
|
||||
# - test_browse_lazy:行合成零 IO/副标题预生成/书库行与阅读器目录册同序(按名定位契约)
|
||||
# - test_browse_perf:500 目录×8 层嵌套 + 2000 项大目录,断言单层扫描与行合成成本
|
||||
# - test_settings_reader_persist:阅读方向 meow_config 持久化(默认/落盘/重启恢复/
|
||||
# 坏值/非法值/只读一次/写失败只告警)
|
||||
# - test_pin:启动 PIN 格式/设置/校验/修改/禁用/残缺配置/落盘失败(logic/pin.c 为纯 C,
|
||||
# 只引用 meow_config_* 声明,测试内以内存 KV 存根顶替 → 不链 SDK)。
|
||||
# - test_settings_lock_host(可选):设置页/锁屏与 SDK view/activity/widget 接线的宿主集成
|
||||
# 验证(owner 归属 + 锁屏 SWIPE 拦截)。需要 SDK **源码树**(不只 meow_ui.a);
|
||||
# 交付环境只有库+头时自动 SKIP。
|
||||
# - test_open_book_host(可选):书库点击「封面图 + 含图子目录」的父目录 → 阅读器打开的
|
||||
# 完整 UI 链路回归(合成夹具,不读真实相册);同样需要 SDK 源码树。
|
||||
# - test_reader_membudget(可选,M22):--wrap malloc 计真实解码峰值 → 单页 ≤3MB 硬阈值、
|
||||
# 连翻 100 页峰值不增长、内存门控/预算跳过可重试、常驻 ≤3MB、取消机制有界。JPEG 夹具由
|
||||
# test/make_mem_samples.py 生成(无 Pillow 时该项 SKIP)。
|
||||
# reader.c 的方向持久化只 include 配置/日志两个模块头;宿主测试用 test/settings_reader_stub.c
|
||||
# 顶替 meow_config_* 与 meow_log(所以下面各条都带 -I"$SDK" 与该存根)。
|
||||
# logic/library.c 的探测缓存用 pthread 互斥锁 → 相关编译带 -pthread。
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
SDK="${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk}"
|
||||
OUT=build/host
|
||||
mkdir -p "$OUT"
|
||||
|
||||
gcc -Wall -Wextra -O1 -I. -I"$SDK" \
|
||||
test/test_library.c logic/library.c logic/reader.c test/settings_reader_stub.c \
|
||||
-pthread -o "$OUT/test_library"
|
||||
"$OUT"/test_library
|
||||
|
||||
gcc -Wall -Wextra -O1 -DMR_LIBRARY_TEST_HOOKS -I. -I"$SDK" \
|
||||
test/test_browse_nav.c logic/library.c logic/reader.c test/settings_reader_stub.c \
|
||||
-pthread -o "$OUT/test_browse_nav"
|
||||
"$OUT"/test_browse_nav
|
||||
|
||||
gcc -Wall -Wextra -O1 -DMR_LIBRARY_TEST_HOOKS -I. -I"$SDK" \
|
||||
test/test_browse_lazy.c logic/library.c logic/reader.c \
|
||||
logic/reader/page_source.c test/settings_reader_stub.c \
|
||||
-pthread -o "$OUT/test_browse_lazy"
|
||||
"$OUT"/test_browse_lazy
|
||||
|
||||
gcc -Wall -Wextra -O1 -DMR_LIBRARY_TEST_HOOKS -I. -I"$SDK" \
|
||||
test/test_browse_perf.c logic/library.c logic/reader.c test/settings_reader_stub.c \
|
||||
-pthread -o "$OUT/test_browse_perf"
|
||||
"$OUT"/test_browse_perf
|
||||
|
||||
gcc -Wall -Wextra -O1 -I. -I"$SDK" \
|
||||
test/test_reader_core.c logic/library.c logic/reader.c \
|
||||
logic/reader/page_source.c logic/reader/nav.c test/settings_reader_stub.c \
|
||||
-pthread -o "$OUT/test_reader_core"
|
||||
"$OUT"/test_reader_core
|
||||
|
||||
gcc -Wall -Wextra -O1 -DMR_READER_TEST_HOOKS -I. -I"$SDK" \
|
||||
test/test_settings_reader_persist.c logic/reader.c test/settings_reader_stub.c \
|
||||
-o "$OUT"/test_settings_reader_persist
|
||||
"$OUT"/test_settings_reader_persist
|
||||
|
||||
gcc -Wall -Wextra -O1 -I. -I"$SDK" \
|
||||
test/test_pin.c logic/pin.c \
|
||||
-o "$OUT"/test_pin
|
||||
"$OUT"/test_pin
|
||||
|
||||
if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then
|
||||
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -I. -I"$SDK" -I"$SDK/include" \
|
||||
test/test_settings_lock_host.c ui/settings/settings.c ui/lock/lock.c \
|
||||
logic/pin.c logic/reader.c \
|
||||
"$SDK"/meow_ui_time.c "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
|
||||
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
|
||||
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
|
||||
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
|
||||
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
|
||||
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
|
||||
"$SDK"/meow_jpeg.c "$SDK"/meow_png_stream.c \
|
||||
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
|
||||
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||
-pthread -lm -lz -o "$OUT"/test_settings_lock_host
|
||||
"$OUT"/test_settings_lock_host
|
||||
|
||||
# 书库浏览页集成(文件管理器行模型/点击语义/根锁定/无 SD 卡空态/零 IO 行合成);同样需要 SDK 源码树。
|
||||
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -DMR_LIBRARY_UI_TEST_HOOKS \
|
||||
-DMR_LIBRARY_TEST_HOOKS \
|
||||
-I. -I"$SDK" -I"$SDK/include" \
|
||||
test/test_browse_ui_host.c ui/library/library.c logic/library.c \
|
||||
logic/reader.c logic/reader/page_source.c logic/reader/archive_source.c \
|
||||
logic/reader/page_cache.c logic/zip/zip.c \
|
||||
"$SDK"/meow_ui_time.c "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
|
||||
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
|
||||
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
|
||||
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
|
||||
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
|
||||
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
|
||||
"$SDK"/meow_jpeg.c "$SDK"/meow_png_stream.c \
|
||||
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
|
||||
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||
-pthread -lm -lz -o "$OUT"/test_browse_ui_host
|
||||
"$OUT"/test_browse_ui_host
|
||||
|
||||
# 点击父目录(封面图 + 含图子目录)→ 阅读器打开:完整 UI 链路回归。
|
||||
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -DMR_READER_HOST_TEST \
|
||||
-DMR_LIBRARY_UI_TEST_HOOKS -I. -I"$SDK" -I"$SDK/include" \
|
||||
test/test_open_book_host.c ui/library/library.c ui/reader/reader.c \
|
||||
logic/library.c logic/reader.c logic/reader/page_source.c \
|
||||
logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c \
|
||||
logic/reader/nav.c \
|
||||
"$SDK"/meow_ui_time.c "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
|
||||
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
|
||||
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
|
||||
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
|
||||
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
|
||||
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
|
||||
"$SDK"/meow_jpeg.c "$SDK"/meow_png_stream.c \
|
||||
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
|
||||
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||
-pthread -lm -lz -o "$OUT"/test_open_book_host
|
||||
"$OUT"/test_open_book_host
|
||||
|
||||
# 翻页热路径(M17):解码=显示尺寸 → 翻页零重采样;fit 切换按新几何重解。
|
||||
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -DMR_READER_HOST_TEST -I. -I"$SDK" -I"$SDK/include" \
|
||||
test/test_reader_perf.c ui/reader/reader.c \
|
||||
logic/library.c logic/reader.c logic/reader/page_source.c \
|
||||
logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c \
|
||||
logic/reader/nav.c \
|
||||
"$SDK"/meow_ui_time.c "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
|
||||
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
|
||||
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
|
||||
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
|
||||
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
|
||||
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
|
||||
"$SDK"/meow_jpeg.c "$SDK"/meow_png_stream.c \
|
||||
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
|
||||
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||
-pthread -lm -lz -o "$OUT"/test_reader_perf
|
||||
"$OUT"/test_reader_perf
|
||||
|
||||
# 140 页夹打开/翻页/跳页 + mr_reader_open_dir_at(文件管理器模式入口)。
|
||||
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -DMR_READER_HOST_TEST -I. -I"$SDK" -I"$SDK/include" \
|
||||
test/test_reader_many_pages.c ui/reader/reader.c \
|
||||
logic/library.c logic/reader.c logic/reader/page_source.c \
|
||||
logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c \
|
||||
logic/reader/nav.c \
|
||||
"$SDK"/meow_ui_time.c "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
|
||||
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
|
||||
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
|
||||
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
|
||||
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
|
||||
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
|
||||
"$SDK"/meow_jpeg.c "$SDK"/meow_png_stream.c \
|
||||
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
|
||||
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||
-pthread -lm -lz -o "$OUT"/test_reader_many_pages
|
||||
"$OUT"/test_reader_many_pages
|
||||
|
||||
# 解码内存预算(M22):--wrap malloc 计真实峰值 → 单页 ≤3MB、连翻 100 页峰值不增长、
|
||||
# 内存门控/预算跳过可重试、常驻 ≤3MB。真机 SIGKILL 事故的回归防线。
|
||||
gcc -Wall -Wextra -O1 -DMEOW_UI_HOST_TEST -DMR_READER_HOST_TEST -I. -I"$SDK" -I"$SDK/include" \
|
||||
test/test_reader_membudget.c ui/reader/reader.c \
|
||||
logic/library.c logic/reader.c logic/reader/page_source.c \
|
||||
logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c \
|
||||
logic/reader/nav.c \
|
||||
"$SDK"/meow_ui_time.c "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
|
||||
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
|
||||
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
|
||||
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
|
||||
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
|
||||
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
|
||||
"$SDK"/meow_jpeg.c "$SDK"/meow_png_stream.c \
|
||||
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
|
||||
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||
-pthread -lm -lz -o "$OUT"/test_reader_membudget \
|
||||
-Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc -Wl,--wrap=free
|
||||
"$OUT"/test_reader_membudget
|
||||
else
|
||||
echo "SKIP test_settings_lock_host/test_browse_ui_host: SDK 源码树不可用(只发布 meow_ui.a 的环境)"
|
||||
fi
|
||||
|
||||
# ---- 档案册(M11):zip 容器 + CBZ 书库/阅读器集成 ----
|
||||
./test/run_zip_tests.sh
|
||||
./test/run_cbz_tests.sh
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# run_webtoon_tests.sh — M7 条漫/模式/续读/跳页宿主测试(纯 C,不链 SDK、不需要设备)。
|
||||
# - test_webtoon_strip:条漫排版(适宽高度/偏移表/可见窗口/未知尺寸兜底/跳页互逆)
|
||||
# - test_jump_page:跳页状态机(预解码落位/超时降级/非法目标)与落位偏移映射
|
||||
# - test_mode_persist:阅读模式与每册 last page 的 meow_config 持久化/坏值/写失败
|
||||
# - test_cont_adjacent:册间自然序边界(首/末册/嵌套/符号链接)+ 书库续读角标
|
||||
# 编译口径同 test/run_tests.sh(-I. + SDK 头,不链 meow_ui.a;配置/日志用存根)。
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
SDK="${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk}"
|
||||
CFLAGS="-Wall -Wextra -O1 -I. -I$SDK"
|
||||
OUT="build/host"
|
||||
mkdir -p "$OUT"
|
||||
|
||||
gcc $CFLAGS test/test_webtoon_strip.c logic/reader/nav.c -o "$OUT/test_webtoon_strip"
|
||||
"$OUT/test_webtoon_strip"
|
||||
|
||||
gcc $CFLAGS test/test_jump_page.c logic/reader/nav.c -o "$OUT/test_jump_page"
|
||||
"$OUT/test_jump_page"
|
||||
|
||||
gcc $CFLAGS test/test_mode_persist.c logic/reader.c test/settings_reader_stub.c \
|
||||
-o "$OUT/test_mode_persist"
|
||||
"$OUT/test_mode_persist"
|
||||
|
||||
gcc $CFLAGS test/test_cont_adjacent.c logic/library.c logic/reader.c \
|
||||
test/settings_reader_stub.c -o "$OUT/test_cont_adjacent"
|
||||
"$OUT/test_cont_adjacent"
|
||||
|
||||
# 可选:真实 SDK 源码树上的阅读器集成测试(模式切换/续读/跳页/册间衔接)。
|
||||
# 只发布 meow_ui.a 的交付环境自动 SKIP(与 test/run_tests.sh 的 settings_lock_host 同口径)。
|
||||
if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then
|
||||
gcc $CFLAGS -DMEOW_UI_HOST_TEST -DMR_READER_HOST_TEST -I"$SDK/include" \
|
||||
test/test_mode_switch_host.c ui/reader/reader.c \
|
||||
logic/library.c logic/reader.c logic/reader/page_source.c \
|
||||
logic/reader/archive_source.c logic/reader/page_cache.c logic/zip/zip.c \
|
||||
logic/reader/nav.c \
|
||||
"$SDK"/meow_ui_time.c "$SDK"/meow_ui_gfx.c "$SDK"/meow_ui_sys.c "$SDK"/meow_ui_theme.c \
|
||||
"$SDK"/meow_ui_vol.c "$SDK"/meow_ui_log.c "$SDK"/meow_ui_widget.c \
|
||||
"$SDK"/meow_ui_input.c "$SDK"/meow_ui_activity.c "$SDK"/meow_ui_dialog.c \
|
||||
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
|
||||
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
|
||||
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
|
||||
"$SDK"/meow_jpeg.c "$SDK"/meow_png_stream.c \
|
||||
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
|
||||
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
|
||||
-pthread -lm -lz -o "$OUT/test_mode_switch_host"
|
||||
"$OUT/test_mode_switch_host"
|
||||
else
|
||||
echo "SKIP test_mode_switch_host: SDK 源码树不可用(只发布 meow_ui.a 的环境)"
|
||||
fi
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
# run_zip_tests.sh — 宿主 gcc 构建并运行 zip 容器读取器测试(纯 C + -lz,无 SDK)。
|
||||
set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
trap 'rm -f test/test_zip' EXIT
|
||||
gcc -Wall -Wextra -Werror -O1 -I. \
|
||||
test/test_zip.c logic/zip/zip.c \
|
||||
-lz -o test/test_zip
|
||||
./test/test_zip ./test/make_zip_samples.py
|
||||
@@ -0,0 +1,92 @@
|
||||
/* settings_reader_stub.c — 宿主测试用 meow_config_* 与 meow_log 最小实现(内存 KV)。
|
||||
*
|
||||
* 只为让 logic/reader.c 在不链 SDK 的宿主测试里可编译/可断言:
|
||||
* - meow_config_get/set:8 个键的内存表,可注入写失败(模拟无卡/只读);
|
||||
* - meow_log:只统计 WARN 次数(验证「写失败只告警、不阻断切换」)。
|
||||
* 与 test_pin.c 自带存根同思路,但抽成共用文件供多个 reader 相关测试链接。
|
||||
*/
|
||||
#include "settings_reader_stub.h"
|
||||
#include "include/meow_ui_config.h"
|
||||
#include "include/meow_ui_log.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define ST_MAX 8
|
||||
static struct { char key[64]; char val[64]; int used; } g_kv[ST_MAX];
|
||||
static int g_fail_writes;
|
||||
static int g_warns;
|
||||
|
||||
static int kv_find(const char *key)
|
||||
{
|
||||
for (int i = 0; i < ST_MAX; i++)
|
||||
if (g_kv[i].used && strcmp(g_kv[i].key, key) == 0) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void mr_test_cfg_clear(void)
|
||||
{
|
||||
memset(g_kv, 0, sizeof g_kv);
|
||||
g_fail_writes = 0;
|
||||
}
|
||||
|
||||
void mr_test_cfg_put(const char *key, const char *val)
|
||||
{
|
||||
if (!key || !key[0]) return;
|
||||
int i = kv_find(key);
|
||||
if (i < 0) {
|
||||
for (i = 0; i < ST_MAX; i++) if (!g_kv[i].used) break;
|
||||
if (i >= ST_MAX) return;
|
||||
g_kv[i].used = 1;
|
||||
snprintf(g_kv[i].key, sizeof g_kv[i].key, "%s", key);
|
||||
}
|
||||
snprintf(g_kv[i].val, sizeof g_kv[i].val, "%s", val ? val : "");
|
||||
}
|
||||
|
||||
int mr_test_cfg_get(const char *key, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return 0;
|
||||
out[0] = '\0';
|
||||
int i = kv_find(key);
|
||||
if (i < 0) return 0;
|
||||
snprintf(out, (size_t)outsz, "%s", g_kv[i].val);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void mr_test_cfg_fail_writes(int on)
|
||||
{
|
||||
g_fail_writes = on ? 1 : 0;
|
||||
}
|
||||
|
||||
int mr_test_log_warns(void)
|
||||
{
|
||||
return g_warns;
|
||||
}
|
||||
|
||||
void mr_test_log_clear(void)
|
||||
{
|
||||
g_warns = 0;
|
||||
}
|
||||
|
||||
/* ---- 被测代码调用的 SDK 接口 ---- */
|
||||
int meow_config_set(const char *key, const char *val)
|
||||
{
|
||||
if (!key || !key[0]) return MEOW_CFG_ERR_INVAL;
|
||||
if (g_fail_writes) return MEOW_CFG_ERR_IO;
|
||||
mr_test_cfg_put(key, val);
|
||||
return MEOW_CFG_OK;
|
||||
}
|
||||
|
||||
int meow_config_get(const char *key, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return MEOW_CFG_ERR_INVAL;
|
||||
out[0] = '\0';
|
||||
if (!mr_test_cfg_get(key, out, outsz)) return MEOW_CFG_ERR_NOENT;
|
||||
return MEOW_CFG_OK;
|
||||
}
|
||||
|
||||
void meow_log(meow_log_level lvl, const char *fmt, ...)
|
||||
{
|
||||
(void)fmt;
|
||||
if (lvl >= MEOW_LOG_WARN) g_warns++;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/* settings_reader_stub.h — 宿主测试用 meow_config_* 与 meow_log 存根(logic/reader.c 的依赖面)。
|
||||
*
|
||||
* 内存 KV(不落盘)+ 测试辅助:读/写/清配置、注入写失败、统计 WARN 次数。
|
||||
* 供 test_library、test_reader_core、test_settings_reader_persist 共用——
|
||||
* 这几个测试都不链 SDK,reader.c 的持久化失败/成功路径靠这里可控。
|
||||
*/
|
||||
#ifndef SETTINGS_READER_STUB_H
|
||||
#define SETTINGS_READER_STUB_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void mr_test_cfg_clear(void); /* 清空全部键 */
|
||||
void mr_test_cfg_put(const char *key, const char *val); /* 直接写入(可注入坏值) */
|
||||
int mr_test_cfg_get(const char *key, char *out, int outsz); /* 1=键存在 */
|
||||
void mr_test_cfg_fail_writes(int on); /* 1=后续 set 全部失败 */
|
||||
int mr_test_log_warns(void); /* meow_log(WARN..) 次数 */
|
||||
void mr_test_log_clear(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif /* SETTINGS_READER_STUB_H */
|
||||
@@ -0,0 +1,294 @@
|
||||
/* test_browse_lazy.c — 浏览行模型 / 零 IO 行合成 / 书库行与阅读器目录册同序(纯 C,无 SDK)。
|
||||
*
|
||||
* 文件名沿用历史(旧的「懒判定」已随 M18 文件管理器模型删除);本测试现在覆盖:
|
||||
* L1 行合成 O(1):mr_library_row_at 纯内存(IO 计数不变)、截断提示行;
|
||||
* L2 **书库行与阅读器目录册同源**:书库图片行的名称序列 == 阅读器目录册
|
||||
* (logic/reader/page_source.c: mr_dir_book_init)的页名序列(同过滤器/同比较器)。
|
||||
* 点图现在由阅读器按文件名自己定位下标,书库不再推导下标;本用例守住「两边枚举一致」。
|
||||
* 夹具含大小写混排(A.jpg/a.jpg)、数字序号(img2/img10)、大写扩展名(IMG3.JPG)、
|
||||
* 前导零(page01/page1)、夹在图片之间的档案册与普通文件;
|
||||
* L3 扫描期副标题:每行都有内容(不再是占位「…」,渲染期不做格式化);
|
||||
* L4 截断安全(评审 P1-1):2100 张图(默认 cap=2000)时,2000 个可见图片行按文件名
|
||||
* 都能定位到同一文件(0 mismatch),被截掉的图仍在阅读器页表里可翻到。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. -I../sdk -DMR_LIBRARY_TEST_HOOKS \
|
||||
* test/test_browse_lazy.c logic/library.c logic/reader/page_source.c \
|
||||
* -pthread -o build/host/test_browse_lazy
|
||||
*/
|
||||
#include "logic/library.h"
|
||||
#include "logic/reader/page_source.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int g_pass, g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
static int mkdir_p(const char *path)
|
||||
{
|
||||
char tmp[MR_PATH_MAX];
|
||||
snprintf(tmp, sizeof tmp, "%s", path);
|
||||
for (size_t i = 1; i <= strlen(tmp); i++) {
|
||||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||||
char ch = tmp[i];
|
||||
tmp[i] = '\0';
|
||||
if (mkdir(tmp, 0755) != 0) {
|
||||
struct stat st;
|
||||
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
|
||||
}
|
||||
tmp[i] = ch;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int touch_file(const char *path)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
fputs("x", f);
|
||||
return fclose(f);
|
||||
}
|
||||
|
||||
static int rm_rf(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) return -1;
|
||||
if (!S_ISDIR(st.st_mode)) return unlink(path);
|
||||
DIR *d = opendir(path);
|
||||
if (!d) return -1;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
|
||||
char full[MR_PATH_MAX];
|
||||
snprintf(full, sizeof full, "%s/%s", path, de->d_name);
|
||||
rm_rf(full);
|
||||
}
|
||||
closedir(d);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
static void joinp(char *out, int outsz, const char *a, const char *b)
|
||||
{
|
||||
snprintf(out, (size_t)outsz, "%s/%s", a, b);
|
||||
}
|
||||
|
||||
static long long now_us(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (long long)ts.tv_sec * 1000000LL + ts.tv_nsec / 1000LL;
|
||||
}
|
||||
|
||||
/* 阅读器目录册用的假解码回调(本测试不打开任何页)。 */
|
||||
static meow_img *fake_load(const char *path, void *ud, int maxdim)
|
||||
{
|
||||
(void)path; (void)ud; (void)maxdim;
|
||||
return NULL;
|
||||
}
|
||||
static void fake_unload(meow_img *img) { (void)img; }
|
||||
|
||||
/* ---- L1 + L3:行合成零 IO / 副标题预生成 ---- */
|
||||
static void test_rows_zero_io(const char *root)
|
||||
{
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
CHECK(mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH) > 0, "scan fixture");
|
||||
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
mr_library_row row;
|
||||
CHECK(mr_library_row_at(&lib, i, &row) == 1, "row %d", i);
|
||||
CHECK(row.title && row.title[0], "row %d title", i);
|
||||
CHECK(row.subtitle && row.subtitle[0] && strcmp(row.subtitle, "…") != 0,
|
||||
"row %d subtitle pre-generated (%s)", i, row.subtitle ? row.subtitle : "");
|
||||
}
|
||||
|
||||
mr_library_io_stats st;
|
||||
mr_library_test_io_reset();
|
||||
long long t0 = now_us();
|
||||
int rows = 0;
|
||||
for (int r = 0; r < 2000; r++)
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
mr_library_row row;
|
||||
if (mr_library_row_at(&lib, i, &row)) rows++;
|
||||
}
|
||||
long long us = now_us() - t0;
|
||||
mr_library_test_io_get(&st);
|
||||
CHECK(rows == 2000 * lib.count, "rows synthesized (%d)", rows);
|
||||
CHECK(st.opendirs == 0 && st.readdirs == 0 && st.lstats == 0,
|
||||
"row synth is pure memory (open=%ld rd=%ld stat=%ld)",
|
||||
st.opendirs, st.readdirs, st.lstats);
|
||||
CHECK(us < 2000000, "row synth bounded (%lldus)", us);
|
||||
|
||||
mr_library_row row;
|
||||
CHECK(mr_library_row_at(&lib, lib.count, &row) == 0, "no extra row when not truncated");
|
||||
lib.truncated = 1;
|
||||
CHECK(mr_library_row_at(&lib, lib.count, &row) == 1 && row.disabled == 1,
|
||||
"truncation info row");
|
||||
CHECK(strcmp(row.title, "已截断") == 0, "truncation title=%s", row.title);
|
||||
CHECK(strstr(row.subtitle, "2000") != NULL, "truncation subtitle=%s", row.subtitle);
|
||||
|
||||
mr_library_free(&lib);
|
||||
}
|
||||
|
||||
/* ---- L2:书库图片行名称序列 == 阅读器目录册页名序列 ---- */
|
||||
static void test_library_rows_match_reader(const char *dir)
|
||||
{
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
CHECK(mr_library_scan(&lib, dir, MR_LIB_MAX_DEPTH) > 0, "scan mixed dir");
|
||||
|
||||
mr_dir_book db;
|
||||
memset(&db, 0, sizeof db);
|
||||
int pages = mr_dir_book_init(&db, dir, 720, fake_load, fake_unload, NULL);
|
||||
CHECK(pages > 0, "reader dir book pages=%d", pages);
|
||||
|
||||
int images = 0;
|
||||
for (int i = 0; i < lib.count; i++)
|
||||
if (lib.items[i].kind == MR_LIB_KIND_IMAGE) images++;
|
||||
CHECK(images == pages, "library images=%d vs reader pages=%d", images, pages);
|
||||
|
||||
/* 逐张对齐:书库第 k 张图 == 阅读器目录册第 k 页(同过滤器/同自然序)。
|
||||
* 点图时阅读器按文件名重新定位下标,所以「名字集合 + 顺序」一致就是契约。 */
|
||||
int k = 0;
|
||||
for (int i = 0; i < lib.count && k < pages; i++) {
|
||||
if (lib.items[i].kind != MR_LIB_KIND_IMAGE) continue;
|
||||
char page_path[MR_PATH_MAX];
|
||||
mr_dir_book_path(&db, k, page_path, (int)sizeof page_path);
|
||||
CHECK(strcmp(page_path, lib.items[i].path) == 0,
|
||||
"page[%d]: library=%s reader=%s", k, lib.items[i].path, page_path);
|
||||
k++;
|
||||
}
|
||||
|
||||
/* 目录/档案册不是页:它们的名字不能出现在阅读器页名里。 */
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
if (lib.items[i].kind == MR_LIB_KIND_IMAGE) continue;
|
||||
int found = 0;
|
||||
for (int j = 0; j < pages; j++)
|
||||
if (db.names[j] && strcmp(db.names[j], lib.items[i].name) == 0) found = 1;
|
||||
CHECK(found == 0, "non-image %s is not a reader page", lib.items[i].name);
|
||||
}
|
||||
|
||||
mr_dir_book_free(&db);
|
||||
mr_library_free(&lib);
|
||||
}
|
||||
|
||||
/* ---- L4:2100 张图(默认 cap)——列表截断但按名定位 2000/2000 不错位 ---- */
|
||||
static void test_cap_name_lookup(const char *root)
|
||||
{
|
||||
char big[MR_PATH_MAX], p[MR_PATH_MAX];
|
||||
joinp(big, sizeof big, root, "big");
|
||||
CHECK(mkdir_p(big) == 0, "mkdir big");
|
||||
for (int i = 1; i <= 2100; i++) {
|
||||
snprintf(p, sizeof p, "%s/p%04d.jpg", big, i);
|
||||
if (touch_file(p) != 0) { CHECK(0, "big image %s", p); return; }
|
||||
}
|
||||
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
int n = mr_library_scan(&lib, big, MR_LIB_MAX_DEPTH);
|
||||
CHECK(n == MR_LIB_SCAN_MAX, "big dir lists cap rows (%d)", n);
|
||||
CHECK(lib.truncated == 1 && lib.count == MR_LIB_SCAN_MAX, "big dir truncated at cap");
|
||||
|
||||
mr_dir_book db;
|
||||
memset(&db, 0, sizeof db);
|
||||
CHECK(mr_dir_book_init(&db, big, 720, fake_load, fake_unload, NULL) == 2100,
|
||||
"reader enumerates all 2100 pages");
|
||||
|
||||
/* 书库不再推导下标;点击传文件名。这里验证 2000 个可见图片行按名定位
|
||||
* 都能命中**同一个文件**(0 mismatch),被截掉的 100 张也仍在页表里。 */
|
||||
int checked = 0, mismatch = 0;
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
if (lib.items[i].kind != MR_LIB_KIND_IMAGE) continue;
|
||||
checked++;
|
||||
int idx = -1;
|
||||
for (int j = 0; j < db.count; j++)
|
||||
if (db.names[j] && strcmp(db.names[j], lib.items[i].name) == 0) { idx = j; break; }
|
||||
char page[MR_PATH_MAX];
|
||||
mr_dir_book_path(&db, idx, page, (int)sizeof page);
|
||||
if (idx < 0 || strcmp(page, lib.items[i].path) != 0) mismatch++;
|
||||
}
|
||||
CHECK(checked == MR_LIB_SCAN_MAX, "checked %d library image rows", checked);
|
||||
CHECK(mismatch == 0, "name lookup mismatch=%d (want 0)", mismatch);
|
||||
|
||||
/* 被截掉的图(列表外的那 100 张)在阅读器页表里仍存在(可翻到)。
|
||||
* 截断发生在 readdir 顺序(非自然序),不假定是哪 100 张,按集合差验证。 */
|
||||
int missing = 0, missing_idx = -1;
|
||||
char missing_name[MR_NAME_MAX] = "";
|
||||
for (int j = 0; j < db.count; j++) {
|
||||
int in_list = 0;
|
||||
for (int i = 0; i < lib.count; i++)
|
||||
if (strcmp(lib.items[i].name, db.names[j]) == 0) { in_list = 1; break; }
|
||||
if (!in_list) {
|
||||
if (missing == 0) {
|
||||
missing_idx = j;
|
||||
snprintf(missing_name, sizeof missing_name, "%s", db.names[j]);
|
||||
}
|
||||
missing++;
|
||||
}
|
||||
}
|
||||
CHECK(missing == 2100 - MR_LIB_SCAN_MAX, "truncated-out images=%d (want 100)", missing);
|
||||
char want_path[MR_PATH_MAX], page[MR_PATH_MAX];
|
||||
snprintf(want_path, sizeof want_path, "%s/%s", big, missing_name);
|
||||
mr_dir_book_path(&db, missing_idx, page, (int)sizeof page);
|
||||
CHECK(missing_idx >= 0 && strcmp(page, want_path) == 0,
|
||||
"truncated-out %s is still reader page %d", missing_name, missing_idx);
|
||||
|
||||
mr_library_row row;
|
||||
CHECK(mr_library_row_at(&lib, lib.count, &row) == 1 && row.disabled == 1 &&
|
||||
strstr(row.subtitle, "2000") && strstr(row.subtitle, "阅读器"),
|
||||
"truncation row keeps images discoverable: %s",
|
||||
row.subtitle ? row.subtitle : "");
|
||||
|
||||
mr_dir_book_free(&db);
|
||||
mr_library_free(&lib);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char tmpl[] = "./mr_rows_XXXXXX";
|
||||
char *root = mkdtemp(tmpl);
|
||||
CHECK(root != NULL, "mkdtemp");
|
||||
if (!root) return 1;
|
||||
|
||||
char p[MR_PATH_MAX];
|
||||
const char *dirs[] = { "book", "mixed", "mixed/sub", "empty" };
|
||||
for (unsigned i = 0; i < sizeof dirs / sizeof dirs[0]; i++) {
|
||||
joinp(p, sizeof p, root, dirs[i]);
|
||||
CHECK(mkdir_p(p) == 0, "mkdir %s", p);
|
||||
}
|
||||
joinp(p, sizeof p, root, "book/page2.png"); CHECK(touch_file(p) == 0, "page2");
|
||||
joinp(p, sizeof p, root, "book/page10.jpg"); CHECK(touch_file(p) == 0, "page10");
|
||||
joinp(p, sizeof p, root, "book/notes.txt"); CHECK(touch_file(p) == 0, "notes");
|
||||
joinp(p, sizeof p, root, "mixed/sub/zz.png");CHECK(touch_file(p) == 0, "nested");
|
||||
|
||||
/* 大小写混排 + 数字序号 + 大写扩展名 + 前导零 + 夹档案/普通文件。 */
|
||||
const char *mixed[] = {
|
||||
"A.jpg", "a.jpg", "img2.png", "IMG3.JPG", "img10.png",
|
||||
"page01.bmp", "page1.jpeg", "cover.cbz", "notes.txt", ".hidden.png"
|
||||
};
|
||||
for (unsigned i = 0; i < sizeof mixed / sizeof mixed[0]; i++) {
|
||||
joinp(p, sizeof p, root, "mixed");
|
||||
size_t dl = strlen(p);
|
||||
snprintf(p + dl, sizeof p - dl, "/%s", mixed[i]);
|
||||
CHECK(touch_file(p) == 0, "image %s", p);
|
||||
}
|
||||
|
||||
test_rows_zero_io(root);
|
||||
joinp(p, sizeof p, root, "mixed");
|
||||
test_library_rows_match_reader(p);
|
||||
test_cap_name_lookup(root);
|
||||
|
||||
rm_rf(root);
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/* test_browse_nav.c — 浏览模型 / 进入目录 / 截断(纯 C,无 SDK)。
|
||||
*
|
||||
* 覆盖(M18 文件管理器模型):
|
||||
* N1 单层列表:目录 + 受支持图片 + 档案册(cbz/zip)都成行,目录优先+自然序;
|
||||
* 系统目录/空目录/无图目录都列出且可进入;隐藏项/普通文件/符号链接不列;
|
||||
* N2 不可读目录/不存在/不是目录:scan 返回 -1,调用方据此显示空态或提示;
|
||||
* N3 父目录路径推导:mr_path_parent 是纯路径工具(可算到文件系统 "/";
|
||||
* 浏览越界的限制在 UI 导航层,见 test_browse_ui_host 的根锁定用例);
|
||||
* N4 单目录条目上限:达到 MR_LIB_SCAN_MAX 后截断并置 truncated(行合成给出提示行)。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. -I../sdk -DMR_LIBRARY_TEST_HOOKS \
|
||||
* test/test_browse_nav.c logic/library.c -pthread -o build/host/test_browse_nav
|
||||
*/
|
||||
#include "logic/library.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int g_pass, g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
static int mkdir_p(const char *path)
|
||||
{
|
||||
char tmp[MR_PATH_MAX];
|
||||
snprintf(tmp, sizeof tmp, "%s", path);
|
||||
for (size_t i = 1; i <= strlen(tmp); i++) {
|
||||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||||
char ch = tmp[i];
|
||||
tmp[i] = '\0';
|
||||
if (mkdir(tmp, 0755) != 0) {
|
||||
struct stat st;
|
||||
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
|
||||
}
|
||||
tmp[i] = ch;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int touch_file(const char *path)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
fputs("x", f);
|
||||
return fclose(f);
|
||||
}
|
||||
|
||||
static int rm_rf(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) return -1;
|
||||
if (!S_ISDIR(st.st_mode)) { chmod(path, 0700); return unlink(path); }
|
||||
chmod(path, 0700);
|
||||
DIR *d = opendir(path);
|
||||
if (!d) return -1;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
|
||||
char full[MR_PATH_MAX];
|
||||
snprintf(full, sizeof full, "%s/%s", path, de->d_name);
|
||||
rm_rf(full);
|
||||
}
|
||||
closedir(d);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
static void joinp(char *out, int outsz, const char *a, const char *b)
|
||||
{
|
||||
snprintf(out, (size_t)outsz, "%s/%s", a, b);
|
||||
}
|
||||
|
||||
static const mr_library_entry *find_entry(const mr_library *lib, const char *name)
|
||||
{
|
||||
for (int i = 0; i < lib->count; i++)
|
||||
if (strcmp(lib->items[i].name, name) == 0) return &lib->items[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---- N1 ---- */
|
||||
static void test_open_access(const char *root)
|
||||
{
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
|
||||
CHECK(n == 9, "root lists 9 rows (got %d)", n);
|
||||
CHECK(lib.truncated == 0, "root not truncated");
|
||||
|
||||
static const char *want[9] = {
|
||||
"book", "empty", "lost+found", "MeowAPPs", "MeowData", "MeowOS", "noimg",
|
||||
"album.cbz", "loose.png"
|
||||
};
|
||||
for (int i = 0; i < 9 && i < lib.count; i++)
|
||||
CHECK(strcmp(lib.items[i].name, want[i]) == 0,
|
||||
"sort[%d]=%s got %s", i, want[i], lib.items[i].name);
|
||||
|
||||
for (int i = 0; i < 7; i++)
|
||||
CHECK(find_entry(&lib, want[i]) != NULL, "system/empty/noimg dir listed: %s", want[i]);
|
||||
CHECK(find_entry(&lib, ".hidden") == NULL, "hidden dir excluded");
|
||||
CHECK(find_entry(&lib, "linkdir") == NULL, "symlink dir excluded");
|
||||
CHECK(find_entry(&lib, "loose.png") != NULL, "loose image listed (clickable)");
|
||||
CHECK(find_entry(&lib, "album.cbz") != NULL, "archive listed");
|
||||
CHECK(find_entry(&lib, "readme.txt") == NULL, "plain file excluded");
|
||||
|
||||
/* 可进入:每个列出的目录都能被 scan(0 项也算成功,不等于 -1)。 */
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
if (lib.items[i].kind != MR_LIB_KIND_DIR) continue;
|
||||
mr_library child;
|
||||
memset(&child, 0, sizeof child);
|
||||
CHECK(mr_library_scan(&child, lib.items[i].path, MR_LIB_MAX_DEPTH) >= 0,
|
||||
"enter %s", lib.items[i].name);
|
||||
mr_library_free(&child);
|
||||
}
|
||||
|
||||
char p[MR_PATH_MAX];
|
||||
joinp(p, sizeof p, root, "book");
|
||||
mr_library book;
|
||||
memset(&book, 0, sizeof book);
|
||||
CHECK(mr_library_scan(&book, p, MR_LIB_MAX_DEPTH) == 1 &&
|
||||
find_entry(&book, "001.png") != NULL, "dir with images lists the image");
|
||||
mr_library_free(&book);
|
||||
|
||||
joinp(p, sizeof p, root, "MeowOS");
|
||||
mr_library syslib;
|
||||
memset(&syslib, 0, sizeof syslib);
|
||||
CHECK(mr_library_scan(&syslib, p, MR_LIB_MAX_DEPTH) == 2 &&
|
||||
find_entry(&syslib, "sub") != NULL &&
|
||||
find_entry(&syslib, "001.png") != NULL, "system dir browsable");
|
||||
mr_library_free(&syslib);
|
||||
|
||||
joinp(p, sizeof p, root, "empty");
|
||||
mr_library empty;
|
||||
memset(&empty, 0, sizeof empty);
|
||||
CHECK(mr_library_scan(&empty, p, MR_LIB_MAX_DEPTH) == 0, "empty dir enterable (0 items)");
|
||||
mr_library_free(&empty);
|
||||
|
||||
mr_library_free(&lib);
|
||||
}
|
||||
|
||||
/* ---- N2 ---- */
|
||||
static void test_unreadable(const char *root)
|
||||
{
|
||||
mr_library lib;
|
||||
char p[MR_PATH_MAX];
|
||||
memset(&lib, 0, sizeof lib);
|
||||
CHECK(mr_library_scan(&lib, "/no/such/dir/m18", MR_LIB_MAX_DEPTH) == -1, "missing -> -1");
|
||||
CHECK(lib.count == 0 && lib.truncated == 0, "failed scan leaves empty lib");
|
||||
mr_library_free(&lib);
|
||||
|
||||
joinp(p, sizeof p, root, "loose.png");
|
||||
memset(&lib, 0, sizeof lib);
|
||||
CHECK(mr_library_scan(&lib, p, MR_LIB_MAX_DEPTH) == -1, "file (not dir) -> -1");
|
||||
mr_library_free(&lib);
|
||||
|
||||
if (geteuid() != 0) { /* root 下 EACCES 不生效,跳过权限用例 */
|
||||
joinp(p, sizeof p, root, "noread");
|
||||
CHECK(mkdir_p(p) == 0, "mkdir noread");
|
||||
CHECK(chmod(p, 0000) == 0, "chmod 000");
|
||||
memset(&lib, 0, sizeof lib);
|
||||
CHECK(mr_library_scan(&lib, p, MR_LIB_MAX_DEPTH) == -1, "unreadable -> -1");
|
||||
CHECK(lib.count == 0, "unreadable lib empty");
|
||||
mr_library_free(&lib);
|
||||
chmod(p, 0700);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- N3 ---- */
|
||||
static void test_parent_path(void)
|
||||
{
|
||||
char cur[MR_PATH_MAX];
|
||||
snprintf(cur, sizeof cur, "/usr/data/mnt/sd_0/a/b/c");
|
||||
int steps = 0;
|
||||
while (strcmp(cur, "/") != 0 && steps < 32) {
|
||||
char up[MR_PATH_MAX];
|
||||
int r = mr_path_parent(cur, up, (int)sizeof up);
|
||||
CHECK(r == 0, "parent of %s", cur);
|
||||
if (r != 0) break;
|
||||
CHECK(strcmp(up, cur) != 0, "parent strictly shorter at %s", cur);
|
||||
memcpy(cur, up, sizeof cur);
|
||||
steps++;
|
||||
}
|
||||
CHECK(strcmp(cur, "/") == 0, "reached filesystem root ('%s')", cur);
|
||||
CHECK(steps == 7, "steps to / = %d (want 7)", steps);
|
||||
|
||||
char up[MR_PATH_MAX];
|
||||
CHECK(mr_path_parent("/", up, (int)sizeof up) == 0 && strcmp(up, "/") == 0,
|
||||
"root parent stays /");
|
||||
CHECK(mr_path_parent("/usr/data/mnt/sd_0", up, (int)sizeof up) == 0 &&
|
||||
strcmp(up, "/usr/data/mnt") == 0,
|
||||
"path helper is root-agnostic: %s", up);
|
||||
}
|
||||
|
||||
/* ---- N4 ---- */
|
||||
static void test_truncation(void)
|
||||
{
|
||||
CHECK(MR_LIB_SCAN_MAX == 2000, "default cap is 2000 (got %d)", MR_LIB_SCAN_MAX);
|
||||
|
||||
char tmpl[] = "./mr_nav_XXXXXX";
|
||||
char *root = mkdtemp(tmpl);
|
||||
CHECK(root != NULL, "mkdtemp");
|
||||
if (!root) return;
|
||||
|
||||
int total = MR_LIB_SCAN_MAX + 8;
|
||||
char p[MR_PATH_MAX];
|
||||
for (int i = 0; i < total; i++) {
|
||||
snprintf(p, sizeof p, "%s/d%04d", root, i);
|
||||
if (mkdir(p, 0755) != 0) { CHECK(0, "mkdir %s", p); break; }
|
||||
}
|
||||
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
|
||||
CHECK(n == MR_LIB_SCAN_MAX, "cap applied: n=%d", n);
|
||||
CHECK(lib.truncated == 1, "truncated flag set");
|
||||
CHECK(lib.count == MR_LIB_SCAN_MAX, "count == cap");
|
||||
|
||||
mr_library_row row;
|
||||
CHECK(mr_library_row_at(&lib, lib.count, &row) == 1 && row.disabled == 1,
|
||||
"truncation row present");
|
||||
CHECK(strcmp(row.title, "已截断") == 0, "truncation row title=%s", row.title);
|
||||
|
||||
mr_library_free(&lib);
|
||||
rm_rf(root);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char tmpl[] = "./mr_nav_XXXXXX";
|
||||
char *root = mkdtemp(tmpl);
|
||||
CHECK(root != NULL, "mkdtemp");
|
||||
if (!root) return 1;
|
||||
|
||||
char p[MR_PATH_MAX];
|
||||
const char *dirs[] = { "MeowAPPs", "MeowData", "MeowOS/sub", "lost+found",
|
||||
"book", "empty", "noimg", ".hidden" };
|
||||
for (unsigned i = 0; i < sizeof dirs / sizeof dirs[0]; i++) {
|
||||
joinp(p, sizeof p, root, dirs[i]);
|
||||
CHECK(mkdir_p(p) == 0, "mkdir %s", p);
|
||||
}
|
||||
joinp(p, sizeof p, root, "MeowOS/001.png");
|
||||
touch_file(p);
|
||||
joinp(p, sizeof p, root, "book/001.png");
|
||||
touch_file(p);
|
||||
joinp(p, sizeof p, root, "noimg/readme.txt");
|
||||
touch_file(p);
|
||||
joinp(p, sizeof p, root, "loose.png");
|
||||
touch_file(p);
|
||||
joinp(p, sizeof p, root, "album.cbz");
|
||||
touch_file(p);
|
||||
joinp(p, sizeof p, root, "readme.txt");
|
||||
touch_file(p);
|
||||
/* 符号链接目录不参与浏览(有权限才建;建不了不影响其余断言)。 */
|
||||
char target[MR_PATH_MAX], link[MR_PATH_MAX];
|
||||
joinp(target, sizeof target, root, "book");
|
||||
joinp(link, sizeof link, root, "linkdir");
|
||||
symlink(target, link);
|
||||
|
||||
test_open_access(root);
|
||||
test_unreadable(root);
|
||||
test_parent_path();
|
||||
|
||||
rm_rf(root);
|
||||
test_truncation();
|
||||
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/* test_browse_perf.c — 单层扫描 / 行合成性能与有界性验收(纯 C,无 SDK)。
|
||||
*
|
||||
* 夹具:500 个顶层目录 × 8 层嵌套(4000 个子目录)+ 根目录 2000 个普通文件
|
||||
* + 一个 big/ 目录(2000 张图片,验证 2000 项大目录)。
|
||||
* 断言(对应 M18 任务③):
|
||||
* P1 首帧扫描只做单层:opendir 次数 == 1,lstat 次数 == 本层条目数,耗时有界;
|
||||
* P2 2000 项大目录:一次扫描出全部 2000 行,opendir=1、lstat=2000,耗时有界;
|
||||
* P3 行合成(UI src 的数据面)纯内存、零 IO、耗时有界(2000 行 × 1000 遍);
|
||||
* P4 进入深层/普通子目录同样只打开它自己(进入目录成本 = 该目录一层)。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. -I../sdk -DMR_LIBRARY_TEST_HOOKS \
|
||||
* test/test_browse_perf.c logic/library.c -pthread -o build/host/test_browse_perf
|
||||
*/
|
||||
#include "logic/library.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define PERF_DIRS 500
|
||||
#define PERF_DEPTH 8
|
||||
#define PERF_FILES 2000
|
||||
#define PERF_BIG 2000
|
||||
|
||||
static int g_pass, g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
static int mkdir_p(const char *path)
|
||||
{
|
||||
char tmp[MR_PATH_MAX];
|
||||
snprintf(tmp, sizeof tmp, "%s", path);
|
||||
for (size_t i = 1; i <= strlen(tmp); i++) {
|
||||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||||
char ch = tmp[i];
|
||||
tmp[i] = '\0';
|
||||
if (mkdir(tmp, 0755) != 0) {
|
||||
struct stat st;
|
||||
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
|
||||
}
|
||||
tmp[i] = ch;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int touch_file(const char *path)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
fputs("x", f);
|
||||
return fclose(f);
|
||||
}
|
||||
|
||||
static int rm_rf(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) return -1;
|
||||
if (!S_ISDIR(st.st_mode)) return unlink(path);
|
||||
DIR *d = opendir(path);
|
||||
if (!d) return -1;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
|
||||
char full[MR_PATH_MAX];
|
||||
snprintf(full, sizeof full, "%s/%s", path, de->d_name);
|
||||
rm_rf(full);
|
||||
}
|
||||
closedir(d);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
static long long now_us(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (long long)ts.tv_sec * 1000000LL + ts.tv_nsec / 1000LL;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char tmpl[] = "./mr_perf_XXXXXX";
|
||||
char *root = mkdtemp(tmpl);
|
||||
CHECK(root != NULL, "mkdtemp");
|
||||
if (!root) return 1;
|
||||
|
||||
char p[MR_PATH_MAX];
|
||||
/* 根目录:2000 个普通文件(不成行)+ 500 个顶层目录(每个顶层目录内是 8 层嵌套 + 1 张图)。 */
|
||||
for (int f = 0; f < PERF_FILES; f++) {
|
||||
snprintf(p, sizeof p, "%s/f%04d.txt", root, f);
|
||||
if (touch_file(p) != 0) { CHECK(0, "file %s", p); break; }
|
||||
}
|
||||
for (int d = 0; d < PERF_DIRS; d++) {
|
||||
char deep[MR_PATH_MAX];
|
||||
snprintf(deep, sizeof deep, "%s/d%04d", root, d);
|
||||
if (mkdir_p(deep) != 0) { CHECK(0, "mkdir %s", deep); break; }
|
||||
snprintf(p, sizeof p, "%s/001.png", deep);
|
||||
if (touch_file(p) != 0) { CHECK(0, "touched top image"); break; }
|
||||
for (int l = 1; l <= PERF_DEPTH; l++) {
|
||||
char sub[MR_PATH_MAX];
|
||||
snprintf(sub, sizeof sub, "%s/l%d", deep, l);
|
||||
if (mkdir(sub, 0755) != 0) { CHECK(0, "mkdir %s", sub); break; }
|
||||
snprintf(deep, sizeof deep, "%s", sub);
|
||||
}
|
||||
snprintf(p, sizeof p, "%s/001.png", deep);
|
||||
if (touch_file(p) != 0) { CHECK(0, "touched deep image"); break; }
|
||||
}
|
||||
|
||||
/* 2000 项大目录(图片)。 */
|
||||
char big[MR_PATH_MAX];
|
||||
snprintf(big, sizeof big, "%s/big", root);
|
||||
CHECK(mkdir_p(big) == 0, "mkdir big");
|
||||
for (int i = 0; i < PERF_BIG; i++) {
|
||||
snprintf(p, sizeof p, "%s/img%04d.png", big, i);
|
||||
if (touch_file(p) != 0) { CHECK(0, "big image %s", p); break; }
|
||||
}
|
||||
|
||||
/* ---- P1:首帧扫描只做单层 ---- */
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
mr_library_test_io_reset();
|
||||
long long t0 = now_us();
|
||||
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
|
||||
long long scan_us = now_us() - t0;
|
||||
mr_library_io_stats st;
|
||||
mr_library_test_io_get(&st);
|
||||
|
||||
CHECK(n == PERF_DIRS + 1, "scan lists %d dirs (got %d)", PERF_DIRS + 1, n);
|
||||
CHECK(lib.truncated == 0, "not truncated");
|
||||
CHECK(st.opendirs == 1, "single-layer scan: opendir=%ld (want 1)", st.opendirs);
|
||||
CHECK(st.lstats == PERF_FILES + PERF_DIRS + 1,
|
||||
"lstat only current level: %ld (want %d)", st.lstats, PERF_FILES + PERF_DIRS + 1);
|
||||
CHECK(scan_us < 1500000LL, "scan bounded: %lldus", scan_us);
|
||||
if (lib.count == PERF_DIRS + 1) {
|
||||
CHECK(strcmp(lib.items[0].name, "big") == 0, "first=%s", lib.items[0].name);
|
||||
CHECK(strcmp(lib.items[lib.count - 1].name, "d0499") == 0,
|
||||
"last=%s", lib.items[lib.count - 1].name);
|
||||
}
|
||||
printf(" root scan: %lldus, %d rows, opendir=%ld lstat=%ld\n",
|
||||
scan_us, n, st.opendirs, st.lstats);
|
||||
mr_library_free(&lib);
|
||||
|
||||
/* ---- P2:2000 项大目录(进入目录 = 单层扫描) ---- */
|
||||
mr_library biglib;
|
||||
memset(&biglib, 0, sizeof biglib);
|
||||
mr_library_test_io_reset();
|
||||
long long b0 = now_us();
|
||||
int bn = mr_library_scan(&biglib, big, MR_LIB_MAX_DEPTH);
|
||||
long long big_us = now_us() - b0;
|
||||
mr_library_test_io_get(&st);
|
||||
CHECK(bn == PERF_BIG, "big dir rows=%d (want %d)", bn, PERF_BIG);
|
||||
CHECK(biglib.truncated == 0, "big dir not truncated at cap");
|
||||
CHECK(st.opendirs == 1, "big dir scan: opendir=%ld", st.opendirs);
|
||||
CHECK(st.lstats == PERF_BIG, "big dir scan: lstat=%ld (want %d)", st.lstats, PERF_BIG);
|
||||
CHECK(big_us < 1500000LL, "big dir scan bounded: %lldus", big_us);
|
||||
if (biglib.count == PERF_BIG) {
|
||||
CHECK(strcmp(biglib.items[0].name, "img0000.png") == 0, "first=%s", biglib.items[0].name);
|
||||
CHECK(strcmp(biglib.items[PERF_BIG - 1].name, "img1999.png") == 0,
|
||||
"last=%s", biglib.items[PERF_BIG - 1].name);
|
||||
CHECK(biglib.items[PERF_BIG - 1].kind == MR_LIB_KIND_IMAGE, "last is image");
|
||||
}
|
||||
printf(" big dir scan: %lldus, %d rows, opendir=%ld lstat=%ld\n",
|
||||
big_us, bn, st.opendirs, st.lstats);
|
||||
|
||||
/* ---- P3:行合成(UI src 数据面)纯内存、2000 行 × 1000 遍 ---- */
|
||||
mr_library_test_io_reset();
|
||||
long long r0 = now_us();
|
||||
int rows = 0;
|
||||
for (int r = 0; r < 1000; r++)
|
||||
for (int i = 0; i < biglib.count; i++) {
|
||||
mr_library_row row;
|
||||
if (mr_library_row_at(&biglib, i, &row)) rows++;
|
||||
}
|
||||
long long row_us = now_us() - r0;
|
||||
mr_library_test_io_get(&st);
|
||||
CHECK(rows == 1000 * biglib.count, "rows synthesized (%d)", rows);
|
||||
CHECK(st.opendirs == 0 && st.readdirs == 0 && st.lstats == 0,
|
||||
"row synthesis does no IO (open=%ld rd=%ld stat=%ld)",
|
||||
st.opendirs, st.readdirs, st.lstats);
|
||||
CHECK(row_us < 1000000LL, "row synthesis bounded: %lldus", row_us);
|
||||
printf(" rows: %lldus for %d rows, zero IO\n", row_us, rows);
|
||||
mr_library_free(&biglib);
|
||||
|
||||
/* ---- P4:进入子目录只打开它自己(不递归子树) ---- */
|
||||
char deep[MR_PATH_MAX];
|
||||
snprintf(deep, sizeof deep, "%s/d0000", root);
|
||||
for (int l = 1; l <= PERF_DEPTH; l++) {
|
||||
char sub[MR_PATH_MAX];
|
||||
snprintf(sub, sizeof sub, "%s/l%d", deep, l);
|
||||
snprintf(deep, sizeof deep, "%s", sub);
|
||||
}
|
||||
mr_library_test_io_reset();
|
||||
mr_library deep_lib;
|
||||
memset(&deep_lib, 0, sizeof deep_lib);
|
||||
CHECK(mr_library_scan(&deep_lib, deep, MR_LIB_MAX_DEPTH) == 1, "deep leaf has one image");
|
||||
mr_library_test_io_get(&st);
|
||||
CHECK(st.opendirs == 1, "deep scan opens once (got %ld)", st.opendirs);
|
||||
CHECK(st.lstats == 1, "deep scan lstats only its image (got %ld)", st.lstats);
|
||||
mr_library_free(&deep_lib);
|
||||
|
||||
/* 顶层目录:一个子目录 + 一张图 = 2 行,只打开它自己。 */
|
||||
snprintf(deep, sizeof deep, "%s/d0000", root);
|
||||
memset(&deep_lib, 0, sizeof deep_lib);
|
||||
mr_library_test_io_reset();
|
||||
CHECK(mr_library_scan(&deep_lib, deep, MR_LIB_MAX_DEPTH) == 2, "top dir rows");
|
||||
mr_library_test_io_get(&st);
|
||||
CHECK(st.opendirs == 1 && st.lstats == 2, "top dir scan: open=%ld stat=%ld",
|
||||
st.opendirs, st.lstats);
|
||||
mr_library_free(&deep_lib);
|
||||
|
||||
rm_rf(root);
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
/* test_browse_ui_host.c — 书库浏览页宿主集成测试(可选,需 SDK 源码树)。
|
||||
*
|
||||
* 目的(M18 文件管理器模型):
|
||||
* U1 行模型:目录 + 图片 + 档案册混排(目录优先+自然序),行副标题是扫描期生成的
|
||||
* 类型·大小(不再有占位/后台补位),图标/右侧箭头按类型区分;
|
||||
* U2 行合成零 IO:反复取可见行不触发 opendir/readdir/lstat(渲染期无文件访问);
|
||||
* U3 点击语义:点目录=进入(空目录也进);点图片=从该图开始读
|
||||
* (mr_reader_open_dir_at_name(当前目录, 文件名);阅读器自己定位下标);
|
||||
* 点档案册=打开档案册;
|
||||
* U4 根目录锁定:根不显示顶栏返回、library_go_up 在根返回 0(app.c 退出);
|
||||
* 子目录返回=回根;不存在越过浏览根的导航;
|
||||
* U5 无 SD 卡容错:浏览根不可访问(不存在/不是目录)→ 友好空态「未检测到 SD 卡」,
|
||||
* 不弹错误横幅;顶栏刷新/重扫可重试成功;
|
||||
* U6 子目录不可读:停留原目录 + 顶部提示横幅;
|
||||
* U7 书库不再碰缓存目录:CBZ 解压页缓存 mrpage_* 原样保留,且不写任何 bmp。
|
||||
* U8 目录内续读入口(M33):顶栏「续读」只在当前所在目录有进度时出现(上层列表行
|
||||
* 不加角标),动作数 3/2 随导航切换;点按从续读页打开当前目录;保存页超页数
|
||||
* 钳到末页;空目录/无进度不显示;从阅读器返回后重新判定。
|
||||
*
|
||||
* 编译:见 test/run_tests.sh(链 SDK 源码树 + 打桩;只发布 meow_ui.a 的环境自动 SKIP)。
|
||||
* ui/library/library.c 以 -DMR_LIBRARY_UI_TEST_HOOKS 编译出测试接缝;logic/library.c
|
||||
* 以 -DMR_LIBRARY_TEST_HOOKS 编译出 IO 计数。
|
||||
*/
|
||||
#include "tuku_internal.h"
|
||||
#include "logic/library.h"
|
||||
#include "logic/reader/page_cache.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
|
||||
/* ui/library/library.c 的测试接缝(仅 -DMR_LIBRARY_UI_TEST_HOOKS 时定义) */
|
||||
int library_test_load(const char *dir);
|
||||
void library_test_set_root(const char *root);
|
||||
const char *library_test_root(void);
|
||||
const char *library_test_dir(void);
|
||||
void library_test_set_dir(const char *dir);
|
||||
int library_test_rows(void);
|
||||
int library_test_src(int idx, meow_list_item *item);
|
||||
int library_test_hint_active(void);
|
||||
const char *library_test_hint(void);
|
||||
int library_test_topbar_back(void);
|
||||
const char *library_test_empty_text(void);
|
||||
int library_test_sd_missing(void);
|
||||
int library_test_retry(void);
|
||||
int library_test_go_up(void);
|
||||
int library_test_drag_active(void);
|
||||
int library_test_has_list(void);
|
||||
int library_test_topbar_actions(void);
|
||||
int library_test_resume_page(void);
|
||||
int library_test_topbar_hit(int x, int y);
|
||||
|
||||
/* 顶栏命中枚举(与 ui/library/library.c 的 LIB_TB_* 同值)。 */
|
||||
#define LIB_TB_NONE 0
|
||||
#define LIB_TB_BACK 1
|
||||
#define LIB_TB_RESUME 2
|
||||
#define LIB_TB_REFRESH 3
|
||||
#define LIB_TB_SETTINGS 4
|
||||
|
||||
/* 阅读器入口打桩:记录被打开的册路径 / 起始页 / 按名定位的文件名(本测试不建 reader 页)。 */
|
||||
#define MR_READER_START_RESUME (-1)
|
||||
void mr_reader_open_dir_at(const char *dir, int start_index);
|
||||
void mr_reader_open_dir_at_name(const char *dir, const char *name);
|
||||
static char g_opened[MR_PATH_MAX];
|
||||
static char g_opened_name[MR_NAME_MAX];
|
||||
static int g_opened_start;
|
||||
static int g_opened_n;
|
||||
void mr_reader_open_dir_at(const char *dir, int start_index)
|
||||
{
|
||||
snprintf(g_opened, sizeof g_opened, "%s", dir ? dir : "");
|
||||
g_opened_name[0] = '\0';
|
||||
g_opened_start = start_index;
|
||||
g_opened_n++;
|
||||
}
|
||||
void mr_reader_open_dir_at_name(const char *dir, const char *name)
|
||||
{
|
||||
snprintf(g_opened, sizeof g_opened, "%s", dir ? dir : "");
|
||||
snprintf(g_opened_name, sizeof g_opened_name, "%s", name ? name : "");
|
||||
g_opened_start = MR_READER_START_RESUME;
|
||||
g_opened_n++;
|
||||
}
|
||||
|
||||
static int g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { printf(" PASS\n"); } \
|
||||
else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
/* app.c 的导航上报在测试里不需要网络侧实现。 */
|
||||
void mr_nav_report(void) {}
|
||||
|
||||
static int mkdir_p(const char *path)
|
||||
{
|
||||
char tmp[MR_PATH_MAX];
|
||||
snprintf(tmp, sizeof tmp, "%s", path);
|
||||
for (size_t i = 1; i <= strlen(tmp); i++) {
|
||||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||||
char ch = tmp[i];
|
||||
tmp[i] = '\0';
|
||||
if (mkdir(tmp, 0755) != 0) {
|
||||
struct stat st;
|
||||
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
|
||||
}
|
||||
tmp[i] = ch;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int touch_file(const char *path)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
fputs("x", f);
|
||||
return fclose(f);
|
||||
}
|
||||
|
||||
static int rm_rf(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) return -1;
|
||||
if (!S_ISDIR(st.st_mode)) { chmod(path, 0700); return unlink(path); }
|
||||
chmod(path, 0700);
|
||||
DIR *d = opendir(path);
|
||||
if (!d) return -1;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
|
||||
char full[MR_PATH_MAX];
|
||||
snprintf(full, sizeof full, "%s/%s", path, de->d_name);
|
||||
rm_rf(full);
|
||||
}
|
||||
closedir(d);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
static void joinp(char *out, int outsz, const char *a, const char *b)
|
||||
{
|
||||
snprintf(out, (size_t)outsz, "%s/%s", a, b);
|
||||
}
|
||||
|
||||
static int row_index_for(const char *name)
|
||||
{
|
||||
int rows = library_test_rows();
|
||||
for (int i = 0; i < rows; i++) {
|
||||
meow_list_item it;
|
||||
if (!library_test_src(i, &it)) continue;
|
||||
if (it.title && strcmp(it.title, name) == 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void tap_row(const char *name)
|
||||
{
|
||||
int idx = row_index_for(name);
|
||||
if (idx < 0) { CHECK(0, "row %s not found", name); return; }
|
||||
int list_top = meow_ui_statusbar_height() + 56;
|
||||
library_on_tap(120, list_top + idx * 56 + 28);
|
||||
}
|
||||
|
||||
/* 统计缓存目录里的 bmp(缩略图/解压页)。 */
|
||||
static int count_bmp(const char *dir, int *pages)
|
||||
{
|
||||
DIR *d = opendir(dir);
|
||||
if (!d) return -1;
|
||||
int thumbs = 0, pg = 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
size_t len = strlen(de->d_name);
|
||||
if (len <= 4 || strcmp(de->d_name + len - 4, ".bmp") != 0) continue;
|
||||
if (mr_cache_is_page_name(de->d_name)) pg++;
|
||||
else thumbs++;
|
||||
}
|
||||
closedir(d);
|
||||
if (pages) *pages = pg;
|
||||
return thumbs;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const char *base = getenv("TMPDIR");
|
||||
if (!base || !base[0]) base = ".";
|
||||
char tmpl[512];
|
||||
snprintf(tmpl, sizeof tmpl, "%s/mr_browse_ui_XXXXXX", base);
|
||||
char *data = mkdtemp(tmpl);
|
||||
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
|
||||
setenv("MEOW_DATA_DIR", data, 1);
|
||||
|
||||
char root[MR_PATH_MAX], p[MR_PATH_MAX];
|
||||
snprintf(root, sizeof root, "%s/library", data);
|
||||
const char *dirs[] = { "album", "empty", "noimg", "deep/sub", "Pictures", "Pictures/sub" };
|
||||
for (unsigned i = 0; i < sizeof dirs / sizeof dirs[0]; i++) {
|
||||
joinp(p, sizeof p, root, dirs[i]);
|
||||
if (mkdir_p(p) != 0) { printf("SKIP: mkdir %s failed\n", p); return 0; }
|
||||
}
|
||||
joinp(p, sizeof p, root, "album/001.png"); touch_file(p);
|
||||
joinp(p, sizeof p, root, "album/002.png"); touch_file(p);
|
||||
joinp(p, sizeof p, root, "noimg/readme.txt"); touch_file(p);
|
||||
joinp(p, sizeof p, root, "deep/sub/001.png"); touch_file(p);
|
||||
joinp(p, sizeof p, root, "root.png"); touch_file(p);
|
||||
joinp(p, sizeof p, root, "book.cbz"); touch_file(p);
|
||||
|
||||
/* Pictures:大小写混排 + 数字序号 + 大写扩展名 + 档案册(图片排序契约夹具)。 */
|
||||
const char *pics[] = { "A.jpg", "a.jpg", "img2.png", "IMG3.JPG", "img10.png",
|
||||
"page01.bmp", "page1.jpeg", "cover.cbz", "notes.txt" };
|
||||
for (unsigned i = 0; i < sizeof pics / sizeof pics[0]; i++) {
|
||||
joinp(p, sizeof p, root, "Pictures");
|
||||
size_t dl = strlen(p);
|
||||
snprintf(p + dl, sizeof p - dl, "/%s", pics[i]);
|
||||
touch_file(p);
|
||||
}
|
||||
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
|
||||
|
||||
/* ---- U5:浏览根不可访问 —— 先指向不存在的根,再 create ---- */
|
||||
const char *bad_root = "/no/such/tuku/m18_root";
|
||||
library_test_set_root(bad_root);
|
||||
CHECK(strcmp(library_test_root(), bad_root) == 0, "bad root installed");
|
||||
meow_view *lib = library_view_create();
|
||||
CHECK(library_test_sd_missing() == 1, "unreadable root -> SD missing state");
|
||||
CHECK(library_test_rows() == 0, "empty list rows=%d", library_test_rows());
|
||||
CHECK(library_test_has_list(), "startup still builds a listview");
|
||||
CHECK(strcmp(library_test_dir(), bad_root) == 0, "startup dir=%s", library_test_dir());
|
||||
CHECK(strstr(library_test_empty_text(), "SD") != NULL,
|
||||
"friendly empty state: %s", library_test_empty_text());
|
||||
CHECK(library_test_hint_active() == 0, "no error banner for missing SD");
|
||||
CHECK(library_test_topbar_back() == 0, "no back arrow at storage root");
|
||||
CHECK(library_test_retry() == 0 && library_test_sd_missing() == 1,
|
||||
"retry while still missing stays in empty state (no crash)");
|
||||
|
||||
/* 根路径不是目录(普通文件)同样走 SD 空态。 */
|
||||
char notadir[MR_PATH_MAX];
|
||||
joinp(notadir, sizeof notadir, data, "notadir");
|
||||
touch_file(notadir);
|
||||
library_test_set_root(notadir);
|
||||
library_test_set_dir(notadir);
|
||||
CHECK(library_test_retry() == 0 && library_test_sd_missing() == 1,
|
||||
"root pointing at a file -> SD missing state");
|
||||
|
||||
/* 插卡/挂载恢复:同一路径变成可读目录后,刷新即恢复列表。 */
|
||||
library_test_set_root(root);
|
||||
library_test_set_dir(root);
|
||||
CHECK(library_test_retry() > 0, "retry after storage available loads rows");
|
||||
CHECK(library_test_sd_missing() == 0, "SD missing state cleared");
|
||||
CHECK(strcmp(library_test_dir(), root) == 0, "now browsing root");
|
||||
CHECK(library_test_topbar_back() == 0, "still no back arrow at root");
|
||||
|
||||
meow_view_page_push(ACT_LIBRARY);
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "library active");
|
||||
|
||||
/* ---- U1:行模型(目录优先 + 自然序 + 类型图标/箭头) ---- */
|
||||
CHECK(library_test_load(root) == 1, "load fixture root");
|
||||
CHECK(library_test_rows() == 7, "7 rows (got %d)", library_test_rows());
|
||||
static const char *want[7] = {
|
||||
"album", "deep", "empty", "noimg", "Pictures", "book.cbz", "root.png"
|
||||
};
|
||||
for (int i = 0; i < 7; i++) {
|
||||
meow_list_item it;
|
||||
CHECK(library_test_src(i, &it) == 1, "src row %d", i);
|
||||
CHECK(it.title && strcmp(it.title, want[i]) == 0, "row[%d]=%s (want %s)",
|
||||
i, it.title ? it.title : "(null)", want[i]);
|
||||
CHECK(it.subtitle && it.subtitle[0] && strcmp(it.subtitle, "…") != 0,
|
||||
"row[%d] subtitle pre-generated (%s)", i, it.subtitle ? it.subtitle : "");
|
||||
if (i < 5) {
|
||||
CHECK(it.icon_kind == MEOW_LIST_ICON_FOLDER && it.right == MEOW_LIST_RIGHT_ARROW,
|
||||
"row[%d] is a folder row with enter arrow", i);
|
||||
} else {
|
||||
CHECK(it.icon_kind == MEOW_LIST_ICON_FILE && it.right == MEOW_LIST_RIGHT_NONE,
|
||||
"row[%d] is a file row without child arrow", i);
|
||||
}
|
||||
}
|
||||
int ic = row_index_for("book.cbz");
|
||||
meow_list_item it;
|
||||
CHECK(ic >= 0 && library_test_src(ic, &it) &&
|
||||
strncmp(it.subtitle, "档案册 · ", strlen("档案册 · ")) == 0,
|
||||
"archive subtitle=%s", it.subtitle ? it.subtitle : "");
|
||||
int ir = row_index_for("root.png");
|
||||
CHECK(ir >= 0 && library_test_src(ir, &it) &&
|
||||
strncmp(it.subtitle, "图片 · ", strlen("图片 · ")) == 0,
|
||||
"image subtitle=%s", it.subtitle ? it.subtitle : "");
|
||||
|
||||
/* ---- U2:行合成零 IO(渲染/滚动路径不访问文件系统) ---- */
|
||||
mr_library_test_io_reset();
|
||||
for (int pass = 0; pass < 500; pass++)
|
||||
for (int i = 0; i < library_test_rows(); i++) {
|
||||
meow_list_item tmp;
|
||||
library_test_src(i, &tmp);
|
||||
}
|
||||
mr_library_io_stats io;
|
||||
mr_library_test_io_get(&io);
|
||||
CHECK(io.opendirs == 0 && io.readdirs == 0 && io.lstats == 0,
|
||||
"row source does no IO (open=%ld rd=%ld stat=%ld)",
|
||||
io.opendirs, io.readdirs, io.lstats);
|
||||
|
||||
/* ---- U3:点击语义 ---- */
|
||||
char pics_dir[MR_PATH_MAX];
|
||||
joinp(pics_dir, sizeof pics_dir, root, "Pictures");
|
||||
CHECK(library_test_load(pics_dir) == 1, "entered Pictures");
|
||||
CHECK(strcmp(library_test_dir(), pics_dir) == 0, "current dir=%s", library_test_dir());
|
||||
|
||||
/* 点图片:把**文件名**交给阅读器(阅读器用自己的全量目录册定位起始页;
|
||||
* 书库列表可能被 MR_LIB_SCAN_MAX 截断,所以不再传下标)。 */
|
||||
const char *target = "img10.png";
|
||||
int ti = row_index_for(target);
|
||||
CHECK(ti >= 0, "target image row present");
|
||||
g_opened_n = 0;
|
||||
tap_row(target);
|
||||
CHECK(g_opened_n == 1 && strcmp(g_opened, pics_dir) == 0,
|
||||
"tap image opens reader for current dir (%s)", g_opened);
|
||||
CHECK(strcmp(g_opened_name, target) == 0, "reader gets file name (%s)", g_opened_name);
|
||||
CHECK(g_opened_start == MR_READER_START_RESUME, "name-based open does not pass an index");
|
||||
CHECK(strcmp(library_test_dir(), pics_dir) == 0, "tap image does not navigate");
|
||||
|
||||
g_opened_n = 0;
|
||||
tap_row("cover.cbz");
|
||||
char archive[MR_PATH_MAX];
|
||||
joinp(archive, sizeof archive, pics_dir, "cover.cbz");
|
||||
CHECK(g_opened_n == 1 && strcmp(g_opened, archive) == 0,
|
||||
"tap archive opens archive book (%s)", g_opened);
|
||||
CHECK(g_opened_start == MR_READER_START_RESUME, "archive uses resume start (%d)",
|
||||
g_opened_start);
|
||||
|
||||
/* 点目录 = 进入;空目录也进(不打开阅读器)。 */
|
||||
char sub_dir[MR_PATH_MAX];
|
||||
joinp(sub_dir, sizeof sub_dir, pics_dir, "sub");
|
||||
g_opened_n = 0;
|
||||
tap_row("sub");
|
||||
CHECK(g_opened_n == 0, "tap dir does not open reader");
|
||||
CHECK(strcmp(library_test_dir(), sub_dir) == 0, "entered empty sub dir (%s)",
|
||||
library_test_dir());
|
||||
CHECK(library_test_rows() == 0, "empty dir shows empty list");
|
||||
CHECK(strcmp(library_test_empty_text(), "空文件夹") == 0,
|
||||
"empty dir state: %s", library_test_empty_text());
|
||||
|
||||
/* ---- U4:根目录锁定 ---- */
|
||||
char album_dir[MR_PATH_MAX];
|
||||
joinp(album_dir, sizeof album_dir, root, "album");
|
||||
CHECK(library_test_load(album_dir) == 1, "load album");
|
||||
CHECK(library_test_topbar_back() == 1, "back arrow shown in subdir");
|
||||
CHECK(library_test_go_up() == 1, "go_up from subdir consumed");
|
||||
CHECK(strcmp(library_test_dir(), root) == 0, "back to root");
|
||||
|
||||
/* 根:系统返回 = 退出(返回 0),并保持列表不变。 */
|
||||
CHECK(library_test_go_up() == 0, "go_up at root returns 0 (app exits)");
|
||||
CHECK(strcmp(library_test_dir(), root) == 0, "still at root after exit gesture");
|
||||
CHECK(library_test_topbar_back() == 0, "no back arrow at root");
|
||||
|
||||
/* 顶栏返回:子目录点击返回箭头 = 回根;根上该区域不响应。 */
|
||||
CHECK(library_test_load(album_dir) == 1, "load album again");
|
||||
library_on_tap(20, meow_ui_statusbar_height() + 28);
|
||||
CHECK(strcmp(library_test_dir(), root) == 0, "topbar back returns to root");
|
||||
int root_rows_before = library_test_rows();
|
||||
library_on_tap(20, meow_ui_statusbar_height() + 28);
|
||||
CHECK(strcmp(library_test_dir(), root) == 0 && library_test_rows() == root_rows_before,
|
||||
"topbar back is inert at root");
|
||||
|
||||
/* 嵌套:deep/sub 两级返回。 */
|
||||
tap_row("deep");
|
||||
tap_row("sub");
|
||||
char deep_sub[MR_PATH_MAX], deep_dir[MR_PATH_MAX];
|
||||
joinp(deep_dir, sizeof deep_dir, root, "deep");
|
||||
joinp(deep_sub, sizeof deep_sub, root, "deep/sub");
|
||||
CHECK(strcmp(library_test_dir(), deep_sub) == 0, "entered deep/sub (%s)", library_test_dir());
|
||||
CHECK(library_test_go_up() == 1 && strcmp(library_test_dir(), deep_dir) == 0, "up to deep");
|
||||
CHECK(library_test_go_up() == 1 && strcmp(library_test_dir(), root) == 0, "up to root");
|
||||
|
||||
/* ---- U3c:左缘 24px / 屏底热区不启动列表拖拽 ---- */
|
||||
int list_top = meow_ui_statusbar_height() + 56;
|
||||
int row_y = list_top + 28;
|
||||
library_on_touch_down(120, row_y);
|
||||
CHECK(library_test_drag_active() == 1, "touch inside list starts drag");
|
||||
library_on_touch_down(5, row_y);
|
||||
CHECK(library_test_drag_active() == 0, "left-edge touch does not start drag");
|
||||
library_on_touch_down(23, row_y);
|
||||
CHECK(library_test_drag_active() == 0, "left-edge (23px) does not start drag");
|
||||
library_on_touch_down(24, row_y);
|
||||
CHECK(library_test_drag_active() == 1, "just outside left band starts drag");
|
||||
library_on_touch_down(120, meow_ui_height() - 4);
|
||||
CHECK(library_test_drag_active() == 0, "bottom hotzone does not start drag");
|
||||
|
||||
/* ---- U6:不可读子目录 → 停留 + 提示 ---- */
|
||||
if (geteuid() != 0) {
|
||||
char noread[MR_PATH_MAX];
|
||||
joinp(noread, sizeof noread, root, "noread");
|
||||
CHECK(mkdir(noread, 0755) == 0, "mkdir noread");
|
||||
chmod(noread, 0000);
|
||||
int ok = library_test_load(noread);
|
||||
CHECK(ok == 0, "unreadable dir load fails");
|
||||
CHECK(strcmp(library_test_dir(), root) == 0, "stays in previous dir (%s)",
|
||||
library_test_dir());
|
||||
CHECK(library_test_hint_active(), "hint banner shown: %s", library_test_hint());
|
||||
CHECK(strstr(library_test_hint(), "无法读取") != NULL,
|
||||
"hint text mentions unreadable: %s", library_test_hint());
|
||||
chmod(noread, 0700);
|
||||
} else {
|
||||
printf(" SKIP unreadable-dir case (running as root)\n");
|
||||
}
|
||||
|
||||
/* ---- U7:书库不碰缓存目录(CBZ 解压页缓存原样保留,不写 bmp) ---- */
|
||||
CHECK(mr_cache_init(data) == 0, "page cache init");
|
||||
const char *cdir = mr_cache_dir();
|
||||
CHECK(cdir && cdir[0], "page cache dir: %s", cdir ? cdir : "");
|
||||
if (cdir && cdir[0]) {
|
||||
char page[MR_PATH_MAX + 96];
|
||||
snprintf(page, sizeof page, "%s/mrpage_0123456789abcdef.bmp", cdir);
|
||||
CHECK(touch_file(page) == 0, "create page cache file");
|
||||
int pages_before = 0;
|
||||
count_bmp(cdir, &pages_before);
|
||||
CHECK(pages_before == 1, "precondition 1 page (got %d)", pages_before);
|
||||
|
||||
CHECK(library_test_load(root) == 1, "reload root");
|
||||
for (int i = 0; i < library_test_rows(); i++) {
|
||||
meow_list_item tmp;
|
||||
library_test_src(i, &tmp);
|
||||
}
|
||||
tap_row("album");
|
||||
tap_row("001.png"); /* 打开阅读器入口(打桩) */
|
||||
library_test_go_up();
|
||||
|
||||
int pages_after = 0;
|
||||
int thumbs_after = count_bmp(cdir, &pages_after);
|
||||
CHECK(access(page, R_OK) == 0, "mrpage_* survives book browsing");
|
||||
CHECK(pages_after == 1 && thumbs_after == 0,
|
||||
"library writes no bmp to cache (pages=%d thumbs=%d)", pages_after, thumbs_after);
|
||||
}
|
||||
|
||||
/* ---- U8:目录内续读入口(M33) ----
|
||||
* 顶栏「续读」只在**已进入的目录本身**有阅读进度时出现(不在上层列表的文件夹行
|
||||
* 加角标/按钮——用户明确要求「进入了该文件夹才有显示」)。点按 = 从续读页打开
|
||||
* 当前目录;保存页超出当前图片数(文件被删)→ 钳到末页;空目录/无进度不显示。 */
|
||||
const int tby = meow_ui_statusbar_height() + 28;
|
||||
char empty_dir[MR_PATH_MAX];
|
||||
joinp(empty_dir, sizeof empty_dir, root, "empty");
|
||||
/* 顶栏宽 480、动作从右往左各 48px:设置最右 [422,470)、刷新 [374,422)、
|
||||
* 续读(有进度时)[326,374)。 */
|
||||
const int x_resume = 350, x_refresh = 395, x_settings = 440;
|
||||
|
||||
/* 根没有进度:只有刷新+设置两个动作,续读格不存在(不是置灰占位)。 */
|
||||
CHECK(library_test_load(root) == 1, "U8 load root");
|
||||
CHECK(library_test_topbar_actions() == 2, "no-progress dir has 2 actions (got %d)",
|
||||
library_test_topbar_actions());
|
||||
CHECK(library_test_resume_page() == -1, "no resume page without progress");
|
||||
CHECK(library_test_topbar_hit(x_resume, tby) == LIB_TB_NONE,
|
||||
"no resume hit cell when hidden");
|
||||
CHECK(library_test_topbar_hit(x_refresh, tby) == LIB_TB_REFRESH,
|
||||
"refresh cell intact without resume");
|
||||
CHECK(library_test_topbar_hit(x_settings, tby) == LIB_TB_SETTINGS,
|
||||
"settings cell intact without resume");
|
||||
|
||||
/* album 有 2 张图;写入进度 P2(page=1)后进入 → 3 个动作。 */
|
||||
mr_cont_set_page(album_dir, 1);
|
||||
CHECK(library_test_load(album_dir) == 1, "U8 load album");
|
||||
CHECK(library_test_topbar_actions() == 3, "dir with progress has 3 actions (got %d)",
|
||||
library_test_topbar_actions());
|
||||
CHECK(library_test_resume_page() == 1, "resume page=%d (want 1)",
|
||||
library_test_resume_page());
|
||||
CHECK(library_test_topbar_hit(x_resume, tby) == LIB_TB_RESUME, "resume cell hits");
|
||||
CHECK(library_test_topbar_hit(x_refresh, tby) == LIB_TB_REFRESH,
|
||||
"refresh still hits with 3 actions");
|
||||
CHECK(library_test_topbar_hit(x_settings, tby) == LIB_TB_SETTINGS,
|
||||
"settings still hits with 3 actions");
|
||||
CHECK(library_test_topbar_hit(20, tby) == LIB_TB_BACK, "back cell still hits");
|
||||
|
||||
/* 点续读 = 从当前目录的续读页打开(阅读器按自己的全量目录册再夹持一次)。 */
|
||||
g_opened_n = 0;
|
||||
g_opened[0] = '\0';
|
||||
g_opened_name[0] = '\0';
|
||||
g_opened_start = 12345;
|
||||
library_on_tap(x_resume, tby);
|
||||
CHECK(g_opened_n == 1 && strcmp(g_opened, album_dir) == 0,
|
||||
"tap resume opens current dir (%s)", g_opened);
|
||||
CHECK(g_opened_start == MR_READER_START_RESUME, "resume tap uses resume start (%d)",
|
||||
g_opened_start);
|
||||
CHECK(strcmp(library_test_dir(), album_dir) == 0, "resume tap does not navigate");
|
||||
|
||||
/* 顶栏刷新仍重扫当前目录(下拉/刷新后可见性保持)。 */
|
||||
char extra[MR_PATH_MAX];
|
||||
joinp(extra, sizeof extra, album_dir, "003.png");
|
||||
touch_file(extra);
|
||||
library_on_tap(x_refresh, tby);
|
||||
CHECK(library_test_rows() == 3, "refresh rescans dir after resume (rows=%d)",
|
||||
library_test_rows());
|
||||
CHECK(library_test_topbar_actions() == 3, "resume survives refresh");
|
||||
unlink(extra);
|
||||
|
||||
/* 返回上层:上层没有这层的进度 → 续读隐藏;再进入 → 重新出现。 */
|
||||
CHECK(library_test_go_up() == 1 && strcmp(library_test_dir(), root) == 0, "up to root");
|
||||
CHECK(library_test_topbar_actions() == 2, "resume hidden in parent without progress");
|
||||
CHECK(library_test_topbar_hit(x_resume, tby) == LIB_TB_NONE, "resume cell gone in parent");
|
||||
CHECK(library_test_load(album_dir) == 1, "re-enter album");
|
||||
CHECK(library_test_topbar_actions() == 3, "resume back after re-enter");
|
||||
|
||||
/* 根也是目录:根有进度时同样显示(root.png 一页,page 0 是有效记录)。 */
|
||||
mr_cont_set_page(root, 0);
|
||||
CHECK(library_test_go_up() == 1 && strcmp(library_test_dir(), root) == 0, "up to root again");
|
||||
CHECK(library_test_topbar_actions() == 3, "root shows resume when it has progress");
|
||||
CHECK(library_test_resume_page() == 0, "root resume page=%d (want 0)",
|
||||
library_test_resume_page());
|
||||
|
||||
/* 边界:保存页超出当前图片数(文件被删)→ 钳到末页(2 页 → 下标 1),仍显示。 */
|
||||
mr_cont_set_page(album_dir, 99);
|
||||
CHECK(library_test_load(album_dir) == 1, "load album with stale page");
|
||||
CHECK(library_test_resume_page() == 1, "stale page clamps to last (got %d)",
|
||||
library_test_resume_page());
|
||||
CHECK(library_test_topbar_actions() == 3, "clamped resume still visible");
|
||||
|
||||
/* 空目录:即使有残留记录也不显示(没有可续读的页)。 */
|
||||
mr_cont_set_page(empty_dir, 2);
|
||||
CHECK(library_test_load(empty_dir) == 1, "load empty dir");
|
||||
CHECK(library_test_resume_page() == -1, "empty dir has no resume entry");
|
||||
CHECK(library_test_topbar_actions() == 2, "empty dir hides resume (got %d)",
|
||||
library_test_topbar_actions());
|
||||
|
||||
/* 从阅读器返回后重判:Pictures 本来没有进度 → 2;阅读器离开时写入进度后,
|
||||
* 走真实 view 生命周期(push reader → pop)→ library on_enter 重判为 3。 */
|
||||
CHECK(library_test_load(pics_dir) == 1, "load Pictures without progress");
|
||||
CHECK(library_test_topbar_actions() == 2, "Pictures initially 2 actions");
|
||||
mr_cont_set_page(pics_dir, 2); /* 模拟阅读器离开时刚写盘 */
|
||||
meow_view *faux_reader = meow_view_create(ACT_READER, MEOW_VIEW_ACTIVITY, NULL, NULL);
|
||||
meow_view_page_push(ACT_READER);
|
||||
meow_view_page_pop();
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back on library after reader");
|
||||
CHECK(library_test_topbar_actions() == 3, "resume appears after reader return (got %d)",
|
||||
library_test_topbar_actions());
|
||||
CHECK(library_test_resume_page() == 2, "resume page after return=%d",
|
||||
library_test_resume_page());
|
||||
meow_view_free(faux_reader);
|
||||
|
||||
meow_view_free(lib);
|
||||
meow_ui_shutdown();
|
||||
rm_rf(data);
|
||||
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
|
||||
g_fail ? "see failures above" : "browse UI checks passed");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,902 @@
|
||||
/* test_cbz.c — M11 档案册(CBZ/zip)集成宿主测试(纯 C + -lz,无 SDK/硬件)。
|
||||
*
|
||||
* 夹具由 test/make_cbz_samples.py 用 python3 zipfile 现场生成(真实 zip 产物 +
|
||||
* 定点字节修补),放在 $TMPDIR 下临时目录,结束后删除。
|
||||
*
|
||||
* 覆盖:
|
||||
* T1 扩展名判据:cbz/zip 大小写不敏感;cbr/gz/无扩展名不算档案。
|
||||
* T2 页枚举:只收图片页(目录项/文本/0 字节/加密/超限过滤),跨子目录自然序;
|
||||
* 中文条目名按原始 UTF-8 字节透传。
|
||||
* T3 路径安全:绝对路径、'..' 段、反斜杠、内嵌隐藏段一律不收;解压字节与
|
||||
* manifest 的 CRC/尺寸一致,缓存文件落在 MeowData/<pkg>/cache。
|
||||
* T4 meta:PNG/BMP/JPEG 原始尺寸经「解压到内存只读头」探测(翻页/条漫排版共用)。
|
||||
* T5 解压缓存纪律:64 页 LRU 淘汰最旧、命中不重写(inode 不变)、32MiB 字节上限、
|
||||
* 启动清孤儿(只删 mrpage_*,不动缩略图 *.bmp)、缓存不可用时优雅失败。
|
||||
* T6 坏档容错:非 zip/截断/无图片页/声明 96MiB 的 zip 炸弹 → 打开失败或 0 页,不崩。
|
||||
* T7 统一开册契约:目录册与档案册走同一个 mr_book(count/meta/open/close/destroy),
|
||||
* 越界 open 返回 NULL,close_auto 幂等。
|
||||
* T8 书库列档(M18 文件管理器模型):目录 + .cbz/.zip 同列(目录优先、组内自然序,
|
||||
* bookA < box < … < Vol10.cbz);扫描期一次成行,副标题是「文件夹/档案册 · 大小」,
|
||||
* 不再探测页数/封面/续读角标;坏档/无图片页档案也照常列出(点开时才由 reader 判)。
|
||||
* T9 册间衔接:同父目录自然序混排(bookA → vol2.cbz → Vol10.cbz),
|
||||
* 首/末册返回 0;不注入探测回调时档案册不参与(M7 语义)。
|
||||
* T10 封面解压 + 无续读角标:档案首图仍可解到缓存;书库行不再带「· 续读 Pn」。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -Werror -O1 -I. test/test_cbz.c logic/library.c logic/reader.c \
|
||||
* logic/reader/page_source.c logic/reader/archive_source.c \
|
||||
* logic/reader/page_cache.c logic/zip/zip.c test/settings_reader_stub.c \
|
||||
* -lz -o test/test_cbz && ./test/test_cbz
|
||||
* 或 test/run_cbz_tests.sh。
|
||||
*/
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "logic/library.h"
|
||||
#include "logic/reader.h"
|
||||
#include "logic/reader/page_cache.h"
|
||||
#include "logic/reader/page_source.h"
|
||||
#include "test/settings_reader_stub.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <zlib.h>
|
||||
|
||||
#define PATH_CAP 1024
|
||||
#define CJK_PAGE "\xe4\xb8\xad\xe6\x96\x87\xe5\x90\x8d.png" /* 中文名.png */
|
||||
#define HUGE_PAGE_LEN (33 * 1024 * 1024) /* 与 make_cbz_samples.py 的 HUGE_PAGE_LEN 一致 */
|
||||
|
||||
static int g_pass;
|
||||
static int g_fail;
|
||||
static char g_root[PATH_CAP]; /* 夹具根 */
|
||||
static char g_lib[PATH_CAP]; /* <root>/library */
|
||||
static char g_feat[PATH_CAP]; /* <root>/features */
|
||||
static char g_data[PATH_CAP]; /* <root>/app/os.meow.picbrowser/data */
|
||||
static const char *g_argv0;
|
||||
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { \
|
||||
g_pass++; \
|
||||
} else { \
|
||||
g_fail++; \
|
||||
printf("FAIL %s:%d ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* ============================================================ */
|
||||
/* 夹具/文件辅助 */
|
||||
/* ============================================================ */
|
||||
static void joinp(char *out, size_t cap, const char *a, const char *b)
|
||||
{
|
||||
snprintf(out, cap, "%s/%s", a, b);
|
||||
}
|
||||
|
||||
static int rm_rf(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) return -1;
|
||||
if (!S_ISDIR(st.st_mode)) return unlink(path);
|
||||
DIR *d = opendir(path);
|
||||
if (!d) return -1;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
|
||||
char full[PATH_CAP];
|
||||
joinp(full, sizeof full, path, de->d_name);
|
||||
rm_rf(full);
|
||||
}
|
||||
closedir(d);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
static int make_temp_dir(void)
|
||||
{
|
||||
const char *bases[3];
|
||||
int nb = 0;
|
||||
const char *env = getenv("TMPDIR");
|
||||
if (env && env[0]) bases[nb++] = env;
|
||||
bases[nb++] = "/data/data/com.termux/files/usr/tmp";
|
||||
bases[nb++] = "/tmp";
|
||||
|
||||
for (int i = 0; i < nb; i++) {
|
||||
char tmpl[PATH_CAP];
|
||||
snprintf(tmpl, sizeof tmpl, "%s/tuku_cbz_XXXXXX", bases[i]);
|
||||
if (mkdtemp(tmpl)) {
|
||||
snprintf(g_root, sizeof g_root, "%s", tmpl);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const char *find_generator(char *buf, size_t cap)
|
||||
{
|
||||
static const char *fallbacks[] = {
|
||||
"test/make_cbz_samples.py", "make_cbz_samples.py",
|
||||
};
|
||||
const char *a0 = g_argv0 ? g_argv0 : "";
|
||||
const char *slash = strrchr(a0, '/');
|
||||
if (a0[0] && strcmp(a0, "-") != 0 && slash) {
|
||||
snprintf(buf, cap, "%.*s/make_cbz_samples.py", (int)(slash - a0), a0);
|
||||
if (access(buf, R_OK) == 0) return buf;
|
||||
}
|
||||
for (size_t i = 0; i < sizeof fallbacks / sizeof fallbacks[0]; i++)
|
||||
if (access(fallbacks[i], R_OK) == 0) return fallbacks[i];
|
||||
return fallbacks[0];
|
||||
}
|
||||
|
||||
static int run_generator(const char *gen)
|
||||
{
|
||||
char cmd[PATH_CAP * 2 + 32];
|
||||
snprintf(cmd, sizeof cmd, "python3 '%s' '%s'", gen, g_root);
|
||||
int st = system(cmd);
|
||||
return st != -1 && WIFEXITED(st) && WEXITSTATUS(st) == 0;
|
||||
}
|
||||
|
||||
static int file_crc(const char *path, unsigned long *crc, long *size)
|
||||
{
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return 0;
|
||||
unsigned char buf[8192];
|
||||
uLong c = crc32(0L, Z_NULL, 0);
|
||||
long total = 0;
|
||||
size_t n;
|
||||
while ((n = fread(buf, 1, sizeof buf, f)) > 0) {
|
||||
c = crc32(c, buf, (uInt)n);
|
||||
total += (long)n;
|
||||
}
|
||||
fclose(f);
|
||||
if (crc) *crc = (unsigned long)c;
|
||||
if (size) *size = total;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* manifest.txt 行:<档案相对路径>\t<条目名>\t<crc32>\t<size> */
|
||||
static int manifest_lookup(const char *rel, const char *entry,
|
||||
unsigned long *crc, long *size)
|
||||
{
|
||||
char path[PATH_CAP];
|
||||
joinp(path, sizeof path, g_root, "manifest.txt");
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return 0;
|
||||
char line[PATH_CAP * 2];
|
||||
char m_rel[PATH_CAP], m_ent[512];
|
||||
unsigned long m_crc;
|
||||
long m_size;
|
||||
int found = 0;
|
||||
while (fgets(line, sizeof line, f) != NULL) {
|
||||
if (sscanf(line, "%1023[^\t]\t%511[^\t]\t%lx\t%ld",
|
||||
m_rel, m_ent, &m_crc, &m_size) != 4)
|
||||
continue;
|
||||
if (strcmp(m_rel, rel) == 0 && strcmp(m_ent, entry) == 0) {
|
||||
if (crc) *crc = m_crc;
|
||||
if (size) *size = m_size;
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 假解码回调(契约边界:logic 不链 SDK) */
|
||||
/* ============================================================ */
|
||||
static int g_dummy;
|
||||
static int g_load_n;
|
||||
static int g_unload_n;
|
||||
static char g_load_path[MR_CACHE_FILE_MAX];
|
||||
|
||||
static meow_img *fake_load(const char *path, void *ud, int maxdim)
|
||||
{
|
||||
(void)ud;
|
||||
(void)maxdim;
|
||||
g_load_n++;
|
||||
snprintf(g_load_path, sizeof g_load_path, "%s", path ? path : "");
|
||||
return (meow_img *)&g_dummy;
|
||||
}
|
||||
|
||||
static void fake_unload(meow_img *img)
|
||||
{
|
||||
if (img) g_unload_n++;
|
||||
}
|
||||
|
||||
static int archive_pages_probe(const char *path, void *ud)
|
||||
{
|
||||
(void)ud;
|
||||
return mr_zip_count_pages(path);
|
||||
}
|
||||
|
||||
static int count_cache_files(void)
|
||||
{
|
||||
const char *dir = mr_cache_dir();
|
||||
if (!dir || !dir[0]) return -1;
|
||||
DIR *d = opendir(dir);
|
||||
if (!d) return -1;
|
||||
int n = 0;
|
||||
size_t plen = strlen(MR_CACHE_PREFIX);
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strncmp(de->d_name, MR_CACHE_PREFIX, plen) == 0) n++;
|
||||
}
|
||||
closedir(d);
|
||||
return n;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* T1 扩展名 */
|
||||
/* ============================================================ */
|
||||
static void test_ext(void)
|
||||
{
|
||||
CHECK(mr_path_is_archive("a.cbz") == 1, "cbz");
|
||||
CHECK(mr_path_is_archive("A.CBZ") == 1, "CBZ");
|
||||
CHECK(mr_path_is_archive("x.zip") == 1, "zip");
|
||||
CHECK(mr_path_is_archive("X.ZiP") == 1, "ZiP");
|
||||
CHECK(mr_path_is_archive("a.cbr") == 0, "cbr not archive");
|
||||
CHECK(mr_path_is_archive("a.gz") == 0, "gz not archive");
|
||||
CHECK(mr_path_is_archive("cbz") == 0, "no extension");
|
||||
CHECK(mr_path_is_archive(".cbz") == 0, "dot name");
|
||||
CHECK(mr_path_is_archive(NULL) == 0, "NULL safe");
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* T2 页枚举 / 排序 / 名字透传 */
|
||||
/* ============================================================ */
|
||||
static void test_enum(void)
|
||||
{
|
||||
char path[PATH_CAP];
|
||||
joinp(path, sizeof path, g_lib, "vol2.cbz");
|
||||
|
||||
mr_zip_book zb;
|
||||
int n = mr_zip_book_init(&zb, path, 720, fake_load, fake_unload, NULL);
|
||||
CHECK(n == 3, "vol2 pages=%d (want 3)", n);
|
||||
CHECK(zb.count == 3, "zb.count=%d", zb.count);
|
||||
if (n != 3) {
|
||||
mr_zip_book_free(&zb);
|
||||
return;
|
||||
}
|
||||
|
||||
char name[512];
|
||||
mr_zip_book_page_name(&zb, 0, name, sizeof name);
|
||||
CHECK(strcmp(name, "ch01/p2.png") == 0, "page0=%s", name);
|
||||
mr_zip_book_page_name(&zb, 1, name, sizeof name);
|
||||
CHECK(strcmp(name, "ch01/p10.png") == 0, "page1=%s", name);
|
||||
mr_zip_book_page_name(&zb, 2, name, sizeof name);
|
||||
CHECK(strcmp(name, CJK_PAGE) == 0, "page2=%s", name);
|
||||
|
||||
mr_zip_book_page_name(&zb, -1, name, sizeof name);
|
||||
CHECK(name[0] == '\0', "page_name(-1) not empty");
|
||||
mr_zip_book_page_name(&zb, 3, name, sizeof name);
|
||||
CHECK(name[0] == '\0', "page_name(count) not empty");
|
||||
mr_zip_book_page_name(&zb, 0, NULL, 10);
|
||||
mr_zip_book_page_name(&zb, 0, name, 0);
|
||||
|
||||
/* 目录项/文本/0 字节条目都被过滤(count==3 已证明)。 */
|
||||
|
||||
mr_book b;
|
||||
memset(&b, 0, sizeof b);
|
||||
mr_zip_book_bind(&b, NULL);
|
||||
CHECK(!mr_book_valid(&b), "bind NULL must leave book invalid");
|
||||
CHECK(mr_book_count(&b) == 0 && mr_book_open(&b, 0) == NULL, "invalid book safe");
|
||||
|
||||
mr_zip_book_bind(&b, &zb);
|
||||
CHECK(mr_book_valid(&b), "valid after bind");
|
||||
CHECK(mr_book_count(&b) == 3, "book count=%d", mr_book_count(&b));
|
||||
CHECK(mr_book_open(&b, 3) == NULL, "open(count) != NULL");
|
||||
CHECK(mr_book_open(&b, -1) == NULL, "open(-1) != NULL");
|
||||
|
||||
int before = g_load_n;
|
||||
meow_img *img = mr_book_open(&b, 0);
|
||||
CHECK(img != NULL, "open(0) returned NULL");
|
||||
CHECK(g_load_n == before + 1, "load callback not called");
|
||||
CHECK(strncmp(g_load_path, mr_cache_dir(), strlen(mr_cache_dir())) == 0,
|
||||
"load path not in cache dir: %s", g_load_path);
|
||||
CHECK(access(g_load_path, R_OK) == 0, "extracted file missing: %s", g_load_path);
|
||||
CHECK(strstr(g_load_path, ".png") != NULL, "extension not preserved: %s", g_load_path);
|
||||
mr_book_close(&b, img);
|
||||
CHECK(g_unload_n >= 1, "unload not called");
|
||||
mr_book_close(&b, NULL); /* 容忍 NULL */
|
||||
|
||||
mr_book_destroy(&b); /* 走 destroy→zip_free */
|
||||
CHECK(!mr_book_valid(&b), "destroy left book valid");
|
||||
mr_zip_book_free(&zb); /* 幂等 */
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* T3 路径安全 + 解压内容 */
|
||||
/* ============================================================ */
|
||||
static void test_pathsafe(void)
|
||||
{
|
||||
char path[PATH_CAP];
|
||||
joinp(path, sizeof path, g_feat, "pathsafe.cbz");
|
||||
|
||||
mr_zip_book zb;
|
||||
int n = mr_zip_book_init(&zb, path, 720, fake_load, fake_unload, NULL);
|
||||
CHECK(n == 2, "pathsafe pages=%d (want 2: ok1.png, sub/ok2.jpg)", n);
|
||||
if (n != 2) {
|
||||
mr_zip_book_free(&zb);
|
||||
return;
|
||||
}
|
||||
|
||||
char name[512];
|
||||
mr_zip_book_page_name(&zb, 0, name, sizeof name);
|
||||
CHECK(strcmp(name, "ok1.png") == 0, "page0=%s", name);
|
||||
mr_zip_book_page_name(&zb, 1, name, sizeof name);
|
||||
CHECK(strcmp(name, "sub/ok2.jpg") == 0, "page1=%s", name);
|
||||
|
||||
const char *bad[] = { "../evil.png", "/abs.png", "a/../../b.png", "sub/../ok3.png",
|
||||
".hidden/x.png", "._mac.png", "sub\\win.png" };
|
||||
for (size_t i = 0; i < sizeof bad / sizeof bad[0]; i++) {
|
||||
CHECK(mr_zip_entry_is_page(bad[i], (unsigned)strlen(bad[i])) == 0,
|
||||
"unsafe entry accepted: %s", bad[i]);
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
char out[MR_CACHE_FILE_MAX];
|
||||
CHECK(mr_zip_book_page_path(&zb, i, out, (int)sizeof out) == 0,
|
||||
"page_path(%d) failed", i);
|
||||
if (!out[0]) continue;
|
||||
CHECK(strncmp(out, mr_cache_dir(), strlen(mr_cache_dir())) == 0,
|
||||
"cache path escaped dir: %s", out);
|
||||
mr_zip_book_page_name(&zb, i, name, sizeof name);
|
||||
unsigned long crc = 0;
|
||||
long size = 0;
|
||||
CHECK(manifest_lookup("features/pathsafe.cbz", name, &crc, &size),
|
||||
"manifest miss %s", name);
|
||||
unsigned long got_crc = 0;
|
||||
long got_size = 0;
|
||||
CHECK(file_crc(out, &got_crc, &got_size), "crc read failed: %s", out);
|
||||
CHECK(got_crc == crc && got_size == size,
|
||||
"extracted bytes differ for %s (crc %lx/%lx size %ld/%ld)",
|
||||
name, got_crc, crc, got_size, size);
|
||||
}
|
||||
mr_zip_book_free(&zb);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* T4 meta(原始像素尺寸) */
|
||||
/* ============================================================ */
|
||||
static void test_meta(void)
|
||||
{
|
||||
char path[PATH_CAP];
|
||||
joinp(path, sizeof path, g_lib, "vol2.cbz");
|
||||
|
||||
mr_zip_book zb;
|
||||
CHECK(mr_zip_book_init(&zb, path, 720, fake_load, fake_unload, NULL) == 3,
|
||||
"vol2 init for meta");
|
||||
mr_book b;
|
||||
mr_zip_book_bind(&b, &zb);
|
||||
|
||||
int w = 0, h = 0;
|
||||
CHECK(mr_book_meta(&b, 0, &w, &h) == 1 && w == 41 && h == 31,
|
||||
"meta0=%dx%d (want 41x31)", w, h);
|
||||
CHECK(mr_book_meta(&b, 1, &w, &h) == 1 && w == 40 && h == 30,
|
||||
"meta1=%dx%d (want 40x30)", w, h);
|
||||
CHECK(mr_book_meta(&b, 2, &w, &h) == 1 && w == 12 && h == 34,
|
||||
"meta2=%dx%d (want 12x34)", w, h);
|
||||
CHECK(mr_book_meta(&b, 3, &w, &h) == 0 && w == 0 && h == 0, "meta(count)");
|
||||
mr_book_destroy(&b);
|
||||
|
||||
joinp(path, sizeof path, g_lib, "Vol10.cbz");
|
||||
CHECK(mr_zip_book_init(&zb, path, 720, fake_load, fake_unload, NULL) == 1,
|
||||
"Vol10 init");
|
||||
mr_zip_book_bind(&b, &zb);
|
||||
CHECK(mr_book_meta(&b, 0, &w, &h) == 1 && w == 7 && h == 9,
|
||||
"bmp meta=%dx%d (want 7x9)", w, h);
|
||||
/* P2-1:BMP 页的缓存文件名也是 *.bmp,但必须能被缩略图 LRU 判据识别为解压页 */
|
||||
char vp[MR_CACHE_FILE_MAX];
|
||||
CHECK(mr_zip_book_page_path(&zb, 0, vp, (int)sizeof vp) == 0, "bmp page path");
|
||||
const char *vbase = strrchr(vp, '/');
|
||||
CHECK(vbase && mr_cache_is_page_name(vbase + 1) == 1,
|
||||
"bmp page cache looks like a thumbnail: %s", vp);
|
||||
CHECK(vbase && strstr(vbase, ".bmp") != NULL, "bmp page lost its extension: %s", vp);
|
||||
mr_book_destroy(&b);
|
||||
|
||||
joinp(path, sizeof path, g_lib, "box/box2.cbz");
|
||||
CHECK(mr_zip_book_init(&zb, path, 720, fake_load, fake_unload, NULL) == 1,
|
||||
"box2 init");
|
||||
mr_zip_book_bind(&b, &zb);
|
||||
CHECK(mr_book_meta(&b, 0, &w, &h) == 1 && w == 12 && h == 34,
|
||||
"jpeg meta=%dx%d (want 12x34)", w, h);
|
||||
mr_book_destroy(&b);
|
||||
|
||||
/* 内存探测边界 */
|
||||
CHECK(mr_probe_size_mem(NULL, 100, &w, &h) == 0, "probe NULL");
|
||||
CHECK(mr_probe_size_mem("ABCDEFGH", 8, &w, &h) == 0, "probe junk");
|
||||
|
||||
/* meta 只解压到内存读头,不落解压缓存(box1 从未被 open 过)。 */
|
||||
joinp(path, sizeof path, g_lib, "box/box1.cbz");
|
||||
CHECK(mr_zip_book_init(&zb, path, 720, fake_load, fake_unload, NULL) == 2,
|
||||
"box1 init");
|
||||
mr_zip_book_bind(&b, &zb);
|
||||
CHECK(mr_book_meta(&b, 0, &w, &h) == 1 && w == 8 && h == 8,
|
||||
"box1 meta=%dx%d (want 8x8)", w, h);
|
||||
struct stat st;
|
||||
char pname[256], cpath[MR_CACHE_FILE_MAX];
|
||||
CHECK(stat(path, &st) == 0, "stat box1");
|
||||
mr_zip_book_page_name(&zb, 0, pname, (int)sizeof pname);
|
||||
CHECK(mr_cache_path(path, (long long)st.st_mtime, (long long)st.st_size,
|
||||
pname, 0, cpath, (int)sizeof cpath) == 0, "meta cache path");
|
||||
CHECK(access(cpath, F_OK) != 0, "meta wrote the page cache");
|
||||
mr_book_destroy(&b);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* T5 解压缓存纪律 */
|
||||
/* ============================================================ */
|
||||
static void test_cache(void)
|
||||
{
|
||||
char path[PATH_CAP];
|
||||
char data_dir[PATH_CAP];
|
||||
char cache_copy[MR_CACHE_DIR_MAX];
|
||||
|
||||
/* 解压页缓存与缩略图的文件名判据(P2-1):缩略图 LRU 依赖它跳过 mrpage_*。 */
|
||||
CHECK(mr_cache_is_page_name("mrpage_0123456789abcdef.bmp") == 1, "page name bmp");
|
||||
CHECK(mr_cache_is_page_name("mrpage_0123456789abcdef.png") == 1, "page name png");
|
||||
CHECK(mr_cache_is_page_name("0123456789abcdef_1700000000_1234.bmp") == 0, "thumb name");
|
||||
CHECK(mr_cache_is_page_name("thumb.bmp") == 0, "plain thumb");
|
||||
CHECK(mr_cache_is_page_name("") == 0 && mr_cache_is_page_name(NULL) == 0,
|
||||
"page name edges");
|
||||
|
||||
joinp(path, sizeof path, g_feat, "big.cbz");
|
||||
|
||||
/* ---- (a) 64 页 LRU ---- */
|
||||
joinp(data_dir, sizeof data_dir, g_root, "c1/os.meow.picbrowser/data");
|
||||
CHECK(mr_cache_init(data_dir) == 0, "cache init c1");
|
||||
snprintf(cache_copy, sizeof cache_copy, "%s", mr_cache_dir());
|
||||
CHECK(cache_copy[0] && strstr(cache_copy, "/cache") != NULL,
|
||||
"cache dir=%s", cache_copy);
|
||||
|
||||
mr_zip_book zb;
|
||||
int n = mr_zip_book_init(&zb, path, 720, fake_load, fake_unload, NULL);
|
||||
CHECK(n == 80, "big pages=%d (want 80)", n);
|
||||
char p[MR_CACHE_FILE_MAX];
|
||||
for (int i = 0; i < n; i++)
|
||||
CHECK(mr_zip_book_page_path(&zb, i, p, (int)sizeof p) == 0,
|
||||
"big page %d path", i);
|
||||
CHECK(mr_cache_count() == MR_CACHE_MAX_PAGES,
|
||||
"cache count=%d (want 64)", mr_cache_count());
|
||||
CHECK(mr_cache_bytes() <= MR_CACHE_MAX_BYTES, "cache bytes over cap");
|
||||
CHECK(count_cache_files() == mr_cache_count(),
|
||||
"disk files=%d index=%d", count_cache_files(), mr_cache_count());
|
||||
|
||||
/* 最旧的 16 页被淘汰,最新的仍在 */
|
||||
struct stat st;
|
||||
char arc[PATH_CAP];
|
||||
char name[512];
|
||||
snprintf(arc, sizeof arc, "%s", path);
|
||||
CHECK(stat(arc, &st) == 0, "stat big.cbz");
|
||||
char p0[MR_CACHE_FILE_MAX], p79[MR_CACHE_FILE_MAX];
|
||||
mr_zip_book_page_name(&zb, 0, name, sizeof name);
|
||||
CHECK(mr_cache_path(arc, (long long)st.st_mtime, (long long)st.st_size,
|
||||
name, 0, p0, (int)sizeof p0) == 0, "cache_path p0");
|
||||
mr_zip_book_page_name(&zb, 79, name, sizeof name);
|
||||
CHECK(mr_cache_path(arc, (long long)st.st_mtime, (long long)st.st_size,
|
||||
name, 79, p79, (int)sizeof p79) == 0, "cache_path p79");
|
||||
CHECK(access(p0, F_OK) != 0, "LRU kept the oldest page");
|
||||
CHECK(access(p79, F_OK) == 0, "LRU dropped the newest page");
|
||||
|
||||
/* 命中不重写:同一 inode */
|
||||
struct stat s1, s2;
|
||||
CHECK(stat(p79, &s1) == 0, "stat p79");
|
||||
CHECK(mr_zip_book_page_path(&zb, 79, p, (int)sizeof p) == 0, "page79 re-get");
|
||||
CHECK(stat(p, &s2) == 0 && s1.st_ino == s2.st_ino, "cache hit rewrote the file");
|
||||
|
||||
/* 假命中自愈(P2-1):外部删掉缓存文件后再取同页,应重新解压而不是返回死路径 */
|
||||
unlink(p79);
|
||||
CHECK(access(p79, F_OK) != 0, "unlink p79");
|
||||
CHECK(mr_zip_book_page_path(&zb, 79, p, (int)sizeof p) == 0, "page79 after unlink");
|
||||
CHECK(access(p79, F_OK) == 0, "page79 not re-extracted after external unlink");
|
||||
CHECK(mr_cache_count() == MR_CACHE_MAX_PAGES, "count after self-heal=%d",
|
||||
mr_cache_count());
|
||||
mr_zip_book_free(&zb);
|
||||
|
||||
/* ---- (b) 32MiB 字节上限 + 启动清孤儿 ---- */
|
||||
joinp(data_dir, sizeof data_dir, g_root, "c2/os.meow.picbrowser/data");
|
||||
mr_cache_shutdown();
|
||||
CHECK(mr_cache_init(data_dir) == 0, "cache init c2");
|
||||
snprintf(cache_copy, sizeof cache_copy, "%s", mr_cache_dir());
|
||||
|
||||
char orphan[MR_CACHE_FILE_MAX], keep[MR_CACHE_FILE_MAX];
|
||||
snprintf(orphan, sizeof orphan, "%s/%sdeadbeef.png", cache_copy, MR_CACHE_PREFIX);
|
||||
snprintf(keep, sizeof keep, "%s/thumb.bmp", cache_copy);
|
||||
FILE *f = fopen(orphan, "wb");
|
||||
CHECK(f != NULL, "create orphan");
|
||||
if (f) { fputs("orphan", f); fclose(f); }
|
||||
f = fopen(keep, "wb");
|
||||
CHECK(f != NULL, "create thumb");
|
||||
if (f) { fputs("thumb", f); fclose(f); }
|
||||
|
||||
mr_cache_shutdown();
|
||||
CHECK(mr_cache_init(data_dir) == 0, "cache re-init c2");
|
||||
CHECK(access(orphan, F_OK) != 0, "startup did not clean orphan mrpage_*");
|
||||
CHECK(access(keep, F_OK) == 0, "startup deleted non-cache file");
|
||||
|
||||
size_t big_len = 20u * 1024u * 1024u;
|
||||
unsigned char *big = (unsigned char *)malloc(big_len);
|
||||
CHECK(big != NULL, "malloc 20MiB");
|
||||
if (big) {
|
||||
memset(big, 0xA5, big_len);
|
||||
char b1[MR_CACHE_FILE_MAX], b2[MR_CACHE_FILE_MAX];
|
||||
snprintf(b1, sizeof b1, "%s/%sbig1.img", cache_copy, MR_CACHE_PREFIX);
|
||||
snprintf(b2, sizeof b2, "%s/%sbig2.img", cache_copy, MR_CACHE_PREFIX);
|
||||
mr_cache_shutdown();
|
||||
mr_cache_init(data_dir);
|
||||
CHECK(mr_cache_store(b1, big, big_len) == 1, "store 20MiB #1");
|
||||
CHECK(mr_cache_count() == 1 && mr_cache_bytes() == (long long)big_len,
|
||||
"count=%d bytes=%lld", mr_cache_count(), mr_cache_bytes());
|
||||
CHECK(mr_cache_store(b2, big, big_len) == 1, "store 20MiB #2");
|
||||
CHECK(mr_cache_count() == 1 && mr_cache_bytes() == (long long)big_len,
|
||||
"byte cap: count=%d bytes=%lld", mr_cache_count(), mr_cache_bytes());
|
||||
CHECK(access(b1, F_OK) != 0, "byte cap did not evict the oldest");
|
||||
free(big);
|
||||
}
|
||||
unlink(keep);
|
||||
|
||||
/* ---- (c) 单页 > 32MiB:文档化的一次性溢出(大页可读,其它条目按预算淘汰) ---- */
|
||||
joinp(data_dir, sizeof data_dir, g_root, "c3/os.meow.picbrowser/data");
|
||||
mr_cache_shutdown();
|
||||
CHECK(mr_cache_init(data_dir) == 0, "cache init c3");
|
||||
|
||||
joinp(arc, sizeof arc, g_lib, "box/box1.cbz");
|
||||
mr_zip_book small;
|
||||
CHECK(mr_zip_book_init(&small, arc, 720, fake_load, fake_unload, NULL) == 2,
|
||||
"small book init");
|
||||
char sp[MR_CACHE_FILE_MAX];
|
||||
CHECK(mr_zip_book_page_path(&small, 0, sp, (int)sizeof sp) == 0, "small page");
|
||||
CHECK(mr_cache_count() == 1, "small cache count=%d", mr_cache_count());
|
||||
mr_zip_book_free(&small);
|
||||
|
||||
joinp(arc, sizeof arc, g_feat, "hugepage.cbz");
|
||||
mr_zip_book huge;
|
||||
CHECK(mr_zip_book_init(&huge, arc, 720, fake_load, fake_unload, NULL) == 1,
|
||||
"hugepage init");
|
||||
char hp[MR_CACHE_FILE_MAX];
|
||||
CHECK(mr_zip_book_page_path(&huge, 0, hp, (int)sizeof hp) == 0, "huge page path");
|
||||
CHECK(mr_cache_count() == 1, "overflow count=%d", mr_cache_count());
|
||||
CHECK(mr_cache_bytes() == (long long)HUGE_PAGE_LEN,
|
||||
"overflow bytes=%lld (want %d)", mr_cache_bytes(), HUGE_PAGE_LEN);
|
||||
CHECK(mr_cache_bytes() > MR_CACHE_MAX_BYTES, "huge page should exceed the budget");
|
||||
CHECK(access(sp, F_OK) != 0, "huge page should evict the small page");
|
||||
CHECK(access(hp, F_OK) == 0, "huge page file missing");
|
||||
CHECK(count_cache_files() == 1, "overflow left extra cache files");
|
||||
|
||||
mr_book hb;
|
||||
mr_zip_book_bind(&hb, &huge);
|
||||
int before_load = g_load_n;
|
||||
meow_img *him = mr_book_open(&hb, 0);
|
||||
CHECK(him != NULL && g_load_n == before_load + 1, "huge page not readable");
|
||||
mr_book_close(&hb, him);
|
||||
mr_book_destroy(&hb);
|
||||
|
||||
/* 再来一个小页:整体预算仍然生效,大页被淘汰 */
|
||||
joinp(arc, sizeof arc, g_lib, "box/box1.cbz");
|
||||
mr_zip_book small2;
|
||||
CHECK(mr_zip_book_init(&small2, arc, 720, fake_load, fake_unload, NULL) == 2,
|
||||
"small2 init");
|
||||
CHECK(mr_zip_book_page_path(&small2, 0, sp, (int)sizeof sp) == 0, "small2 page");
|
||||
CHECK(mr_cache_count() == 1 && mr_cache_bytes() < MR_CACHE_MAX_BYTES,
|
||||
"budget not restored: count=%d bytes=%lld", mr_cache_count(),
|
||||
mr_cache_bytes());
|
||||
CHECK(access(hp, F_OK) != 0, "huge page should be evicted by the budget");
|
||||
mr_zip_book_free(&small2);
|
||||
|
||||
/* ---- (d) 缓存不可用时优雅失败(不崩、不写文件) ---- */
|
||||
mr_cache_shutdown();
|
||||
CHECK(mr_cache_ready() == 0, "shutdown left cache ready");
|
||||
joinp(path, sizeof path, g_lib, "vol2.cbz");
|
||||
mr_zip_book zb2;
|
||||
CHECK(mr_zip_book_init(&zb2, path, 720, fake_load, fake_unload, NULL) == 3,
|
||||
"init without cache");
|
||||
CHECK(mr_zip_book_page_path(&zb2, 0, p, (int)sizeof p) == -1,
|
||||
"page_path without cache should fail");
|
||||
int before = g_load_n;
|
||||
mr_book b;
|
||||
mr_zip_book_bind(&b, &zb2);
|
||||
CHECK(mr_book_open(&b, 0) == NULL, "open without cache should fail");
|
||||
CHECK(g_load_n == before, "decode callback called without cache");
|
||||
mr_book_destroy(&b);
|
||||
|
||||
joinp(data_dir, sizeof data_dir, g_root, "app/os.meow.picbrowser/data");
|
||||
CHECK(mr_cache_init(data_dir) == 0, "cache init app");
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* T6 坏档容错 */
|
||||
/* ============================================================ */
|
||||
static void expect_bad(const char *path, const char *tag)
|
||||
{
|
||||
mr_book_handle h;
|
||||
mr_book b;
|
||||
int n = mr_book_open_auto(&b, &h, path, 720, fake_load, fake_unload, NULL);
|
||||
CHECK(n == -1, "%s: open_auto=%d (want -1)", tag, n);
|
||||
CHECK(!mr_book_valid(&b), "%s: book valid after failure", tag);
|
||||
CHECK(mr_book_open(&b, 0) == NULL, "%s: open after failure", tag);
|
||||
mr_book_close_auto(&b, &h);
|
||||
mr_book_close_auto(&b, &h); /* 幂等 */
|
||||
CHECK(mr_zip_count_pages(path) == 0, "%s: count_pages != 0", tag);
|
||||
}
|
||||
|
||||
static void test_bad(void)
|
||||
{
|
||||
char p[PATH_CAP];
|
||||
|
||||
joinp(p, sizeof p, g_lib, "broken.cbz");
|
||||
expect_bad(p, "broken");
|
||||
joinp(p, sizeof p, g_lib, "truncated.cbz");
|
||||
expect_bad(p, "truncated");
|
||||
joinp(p, sizeof p, g_feat, "truncated.cbz");
|
||||
expect_bad(p, "truncated(feat)");
|
||||
|
||||
joinp(p, sizeof p, g_lib, "missing.cbz");
|
||||
expect_bad(p, "missing");
|
||||
|
||||
mr_book_handle h;
|
||||
mr_book b;
|
||||
CHECK(mr_book_open_auto(&b, &h, NULL, 720, fake_load, fake_unload, NULL) == -1,
|
||||
"open_auto(NULL)");
|
||||
CHECK(mr_book_open_auto(NULL, &h, p, 720, fake_load, fake_unload, NULL) == -1,
|
||||
"open_auto(NULL book)");
|
||||
CHECK(mr_book_open_auto(&b, NULL, p, 720, fake_load, fake_unload, NULL) == -1,
|
||||
"open_auto(NULL handle)");
|
||||
|
||||
/* 合法 zip 但没有图片页 / 声明尺寸超上限(zip 炸弹) */
|
||||
const char *zero[] = { "notabook.zip", "emptypages.cbz" };
|
||||
for (size_t i = 0; i < sizeof zero / sizeof zero[0]; i++) {
|
||||
joinp(p, sizeof p, g_lib, zero[i]);
|
||||
int n = mr_book_open_auto(&b, &h, p, 720, fake_load, fake_unload, NULL);
|
||||
CHECK(n == 0, "%s: open_auto=%d (want 0)", zero[i], n);
|
||||
CHECK(!mr_book_valid(&b), "%s: book bound with 0 pages", zero[i]);
|
||||
CHECK(mr_book_open(&b, 0) == NULL, "%s: open on empty book", zero[i]);
|
||||
mr_book_close_auto(&b, &h);
|
||||
CHECK(mr_zip_count_pages(p) == 0, "%s: count_pages != 0", zero[i]);
|
||||
}
|
||||
|
||||
joinp(p, sizeof p, g_feat, "bomb.cbz");
|
||||
CHECK(mr_zip_count_pages(p) == 0, "bomb.cbz should have no page (96MiB)");
|
||||
CHECK(mr_book_open_auto(&b, &h, p, 720, fake_load, fake_unload, NULL) == 0,
|
||||
"bomb: open_auto should be 0 pages");
|
||||
mr_book_close_auto(&b, &h);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* T7 统一开册契约 */
|
||||
/* ============================================================ */
|
||||
static void test_open_auto(void)
|
||||
{
|
||||
char p[PATH_CAP];
|
||||
joinp(p, sizeof p, g_lib, "bookA");
|
||||
|
||||
mr_book_handle h;
|
||||
mr_book b;
|
||||
int n = mr_book_open_auto(&b, &h, p, 720, fake_load, fake_unload, NULL);
|
||||
CHECK(n == 2 && h.kind == MR_BOOK_KIND_DIR, "dir open n=%d kind=%d", n, h.kind);
|
||||
CHECK(mr_book_valid(&b) && mr_book_count(&b) == 2, "dir book contract");
|
||||
int before = g_load_n;
|
||||
meow_img *img = mr_book_open(&b, 1);
|
||||
CHECK(img != NULL && g_load_n == before + 1, "dir open(1)");
|
||||
CHECK(strstr(g_load_path, "002.png") != NULL, "dir page path=%s", g_load_path);
|
||||
mr_book_close(&b, img);
|
||||
int mw = 0, mh = 0;
|
||||
CHECK(mr_book_meta(&b, 0, &mw, &mh) == 1 && mw == 30 && mh == 20,
|
||||
"dir meta=%dx%d (want 30x20)", mw, mh);
|
||||
mr_book_close_auto(&b, &h);
|
||||
CHECK(h.kind == MR_BOOK_KIND_NONE && !mr_book_valid(&b), "dir close_auto");
|
||||
mr_book_close_auto(&b, &h); /* 幂等 */
|
||||
|
||||
joinp(p, sizeof p, g_lib, "vol2.cbz");
|
||||
n = mr_book_open_auto(&b, &h, p, 720, fake_load, fake_unload, NULL);
|
||||
CHECK(n == 3 && h.kind == MR_BOOK_KIND_ZIP, "zip open n=%d kind=%d", n, h.kind);
|
||||
CHECK(mr_book_valid(&b) && mr_book_count(&b) == 3, "zip book contract");
|
||||
CHECK(mr_book_open(&b, 99) == NULL, "zip out of range");
|
||||
mr_book_close_auto(&b, &h);
|
||||
CHECK(!mr_book_valid(&b), "zip close_auto");
|
||||
|
||||
joinp(p, sizeof p, g_lib, "emptypages.cbz");
|
||||
n = mr_book_open_auto(&b, &h, p, 720, fake_load, fake_unload, NULL);
|
||||
CHECK(n == 0 && h.kind == MR_BOOK_KIND_ZIP, "empty zip n=%d kind=%d", n, h.kind);
|
||||
CHECK(!mr_book_valid(&b), "empty zip bound");
|
||||
mr_book_close_auto(&b, &h);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* T8 书库列档 */
|
||||
/* ============================================================ */
|
||||
static const mr_library_entry *lib_find(const mr_library *lib, const char *name)
|
||||
{
|
||||
for (int i = 0; i < lib->count; i++)
|
||||
if (strcmp(lib->items[i].name, name) == 0) return &lib->items[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void test_library(void)
|
||||
{
|
||||
mr_test_cfg_clear();
|
||||
|
||||
/* 文件管理器模型:目录 + 档案册同列(目录优先、组内自然序),扫描期一次成行;
|
||||
* 档案册只按扩展名成行,不探测页数/封面(打开时才由 reader 解档)。 */
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
int n = mr_library_scan(&lib, g_lib, MR_LIB_MAX_DEPTH);
|
||||
CHECK(n == 8, "library rows=%d (want 2 dirs + 6 archives)", n);
|
||||
static const char *want[8] = {
|
||||
"bookA", "box", "broken.cbz", "emptypages.cbz",
|
||||
"notabook.zip", "truncated.cbz", "vol2.cbz", "Vol10.cbz"
|
||||
};
|
||||
for (int i = 0; i < 8 && i < lib.count; i++) {
|
||||
CHECK(strcmp(lib.items[i].name, want[i]) == 0,
|
||||
"row[%d]=%s (want %s)", i, lib.items[i].name, want[i]);
|
||||
}
|
||||
for (int i = 0; i < 2 && i < lib.count; i++)
|
||||
CHECK(lib.items[i].kind == MR_LIB_KIND_DIR &&
|
||||
strcmp(lib.items[i].subtitle, "文件夹") == 0,
|
||||
"%s dir row subtitle=%s", lib.items[i].name, lib.items[i].subtitle);
|
||||
for (int i = 2; i < 8 && i < lib.count; i++)
|
||||
CHECK(lib.items[i].kind == MR_LIB_KIND_ARCHIVE &&
|
||||
strncmp(lib.items[i].subtitle, "档案册 · ", strlen("档案册 · ")) == 0,
|
||||
"%s archive row subtitle=%s", lib.items[i].name, lib.items[i].subtitle);
|
||||
|
||||
const mr_library_entry *e = lib_find(&lib, "bookA");
|
||||
CHECK(e && e->kind == MR_LIB_KIND_DIR && e->size == 0, "bookA dir entry");
|
||||
|
||||
e = lib_find(&lib, "vol2.cbz");
|
||||
CHECK(e && e->kind == MR_LIB_KIND_ARCHIVE && e->size > 0 &&
|
||||
strstr(e->path, "vol2.cbz") != NULL,
|
||||
"vol2.cbz archive entry (size=%lld)", e ? e->size : -1);
|
||||
|
||||
/* 坏档/无图片页档案也照常列出(书库不预判,点开时 reader 才判可读性)。 */
|
||||
static const char *all_arch[] = { "notabook.zip", "emptypages.cbz",
|
||||
"broken.cbz", "truncated.cbz" };
|
||||
for (unsigned i = 0; i < sizeof all_arch / sizeof all_arch[0]; i++) {
|
||||
e = lib_find(&lib, all_arch[i]);
|
||||
CHECK(e && e->kind == MR_LIB_KIND_ARCHIVE, "%s listed as archive", all_arch[i]);
|
||||
}
|
||||
CHECK(lib_find(&lib, ".hidden.cbz") == NULL, "hidden cbz not listed");
|
||||
mr_library_free(&lib);
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* T9 册间衔接 */
|
||||
/* ============================================================ */
|
||||
static void expect_adj(const char *from, int delta, const char *want, const char *tag)
|
||||
{
|
||||
char out[MR_PATH_MAX];
|
||||
int r = mr_book_adjacent_ex(from, delta, out, (int)sizeof out,
|
||||
archive_pages_probe, NULL);
|
||||
if (!want) {
|
||||
CHECK(r == 0 && out[0] == '\0', "%s: r=%d out=%s", tag, r, out);
|
||||
} else {
|
||||
CHECK(r == 1 && strcmp(out, want) == 0, "%s: got %s (want %s)", tag, out, want);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_adjacent(void)
|
||||
{
|
||||
char bookA[MR_PATH_MAX], vol2[MR_PATH_MAX], vol10[MR_PATH_MAX];
|
||||
char broken[MR_PATH_MAX];
|
||||
joinp(bookA, sizeof bookA, g_lib, "bookA");
|
||||
joinp(vol2, sizeof vol2, g_lib, "vol2.cbz");
|
||||
joinp(vol10, sizeof vol10, g_lib, "Vol10.cbz");
|
||||
joinp(broken, sizeof broken, g_lib, "broken.cbz");
|
||||
|
||||
/* 兄弟表 = bookA + vol2.cbz + Vol10.cbz(box 无直接图、坏档不算书)。 */
|
||||
expect_adj(bookA, 1, vol2, "bookA next");
|
||||
expect_adj(vol2, 1, vol10, "vol2 next");
|
||||
expect_adj(vol10, 1, NULL, "vol10 next (end)");
|
||||
expect_adj(bookA, -1, NULL, "bookA prev (start)");
|
||||
expect_adj(vol2, -1, bookA, "vol2 prev");
|
||||
expect_adj(vol10, -1, vol2, "vol10 prev");
|
||||
expect_adj(broken, 1, NULL, "broken not a book");
|
||||
expect_adj(broken, -1, NULL, "broken not a book prev");
|
||||
|
||||
/* 不注入探测回调:档案册不参与(M7 语义) */
|
||||
char out[MR_PATH_MAX];
|
||||
CHECK(mr_book_adjacent(vol2, 1, out, (int)sizeof out) == 0,
|
||||
"legacy adjacency should ignore archives (r=%d)", 0);
|
||||
CHECK(mr_book_adjacent(bookA, 1, out, (int)sizeof out) == 0,
|
||||
"legacy adjacency bookA next should be none");
|
||||
|
||||
/* 参数边界 */
|
||||
CHECK(mr_book_adjacent_ex(vol2, 0, out, (int)sizeof out, archive_pages_probe,
|
||||
NULL) == -1, "delta 0");
|
||||
CHECK(mr_book_adjacent_ex(NULL, 1, out, (int)sizeof out, archive_pages_probe,
|
||||
NULL) == -1, "NULL dir");
|
||||
CHECK(mr_book_adjacent_ex(vol2, 1, NULL, 32, archive_pages_probe, NULL) == -1,
|
||||
"NULL out");
|
||||
CHECK(mr_book_adjacent_ex("/", 1, out, (int)sizeof out, archive_pages_probe,
|
||||
NULL) == -1, "root");
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* T10 封面解压 + 无续读角标 */
|
||||
/* ============================================================ */
|
||||
static void test_badge_cover(void)
|
||||
{
|
||||
char vol2[MR_PATH_MAX];
|
||||
joinp(vol2, sizeof vol2, g_lib, "vol2.cbz");
|
||||
|
||||
mr_test_cfg_clear();
|
||||
mr_cont_save(vol2, 2, MR_MODE_WEBTOON);
|
||||
|
||||
/* 文件管理器模型:书库行只有「档案册 · 大小」,不再有「· 续读 Pn」角标
|
||||
* (续读仍由 reader 打开该册时恢复,见 test_mode_switch_host)。 */
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
CHECK(mr_library_scan(&lib, g_lib, MR_LIB_MAX_DEPTH) == 8, "rescan rows");
|
||||
const mr_library_entry *e = lib_find(&lib, "vol2.cbz");
|
||||
CHECK(e && strstr(e->subtitle, "续读") == NULL,
|
||||
"no continuation badge in file-manager rows: %s", e ? e->subtitle : "");
|
||||
CHECK(e && strstr(e->subtitle, "档案册") != NULL, "archive subtitle=%s",
|
||||
e ? e->subtitle : "");
|
||||
e = lib_find(&lib, "Vol10.cbz");
|
||||
CHECK(e && strstr(e->subtitle, "续读") == NULL, "Vol10 no badge either");
|
||||
mr_library_free(&lib);
|
||||
|
||||
/* 封面 = 档内自然序首图(ch01/p2.png),解压到缓存 */
|
||||
char cover[MR_CACHE_FILE_MAX];
|
||||
CHECK(mr_zip_cover_path(vol2, cover, (int)sizeof cover) == 1, "cover path");
|
||||
CHECK(cover[0] && access(cover, R_OK) == 0, "cover file missing: %s", cover);
|
||||
unsigned long crc = 0;
|
||||
long size = 0;
|
||||
CHECK(file_crc(cover, &crc, &size), "cover crc");
|
||||
unsigned long want_crc = 0;
|
||||
long want_size = 0;
|
||||
CHECK(manifest_lookup("library/vol2.cbz", "ch01/p2.png", &want_crc, &want_size),
|
||||
"cover manifest");
|
||||
CHECK(crc == want_crc && size == want_size, "cover bytes differ");
|
||||
|
||||
char broken[MR_PATH_MAX];
|
||||
joinp(broken, sizeof broken, g_lib, "broken.cbz");
|
||||
CHECK(mr_zip_cover_path(broken, cover, (int)sizeof cover) == 0, "broken cover");
|
||||
CHECK(mr_zip_cover_path(vol2, NULL, 16) == 0, "cover NULL out");
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
g_argv0 = argv[0];
|
||||
char gen_buf[PATH_CAP];
|
||||
const char *gen = argc > 1 ? argv[1] : find_generator(gen_buf, sizeof gen_buf);
|
||||
|
||||
if (!make_temp_dir()) {
|
||||
perror("mkdtemp");
|
||||
return 2;
|
||||
}
|
||||
joinp(g_lib, sizeof g_lib, g_root, "library");
|
||||
joinp(g_feat, sizeof g_feat, g_root, "features");
|
||||
joinp(g_data, sizeof g_data, g_root, "app/os.meow.picbrowser/data");
|
||||
|
||||
if (!run_generator(gen)) {
|
||||
printf("generator failed: %s\n", gen);
|
||||
rm_rf(g_root);
|
||||
return 2;
|
||||
}
|
||||
if (mr_cache_init(g_data) != 0) {
|
||||
printf("cache init failed: %s\n", g_data);
|
||||
rm_rf(g_root);
|
||||
return 2;
|
||||
}
|
||||
|
||||
test_ext();
|
||||
test_enum();
|
||||
test_pathsafe();
|
||||
test_meta();
|
||||
test_cache();
|
||||
test_bad();
|
||||
test_open_auto();
|
||||
test_library();
|
||||
test_adjacent();
|
||||
test_badge_cover();
|
||||
|
||||
mr_cache_shutdown();
|
||||
rm_rf(g_root);
|
||||
|
||||
printf("cbz tests: %d passed, %d failed\n", g_pass, g_fail);
|
||||
return g_fail == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
/* test_cont_adjacent.c — 册间顺序边界 + 书库续读角标宿主测试(无 SDK/硬件)。
|
||||
*
|
||||
* 覆盖:
|
||||
* A1 「直接含图」判定:含图目录=书;只有子目录含图/空目录/仅非图文件=不是书;
|
||||
* A2 兄弟册排序:同父目录、书库自然序(vol2 < vol10);首/末册无相邻返回 0;
|
||||
* A3 边界:末页前进→下一册、首页后退→上一册(调用方语义);路径末尾斜杠等价;
|
||||
* A4 排除:隐藏/系统目录、符号链接、普通文件不参与兄弟册;
|
||||
* A5 参数:delta 非 ±1、NULL/小缓冲、不存在的目录;
|
||||
* A6 书库行模型(M18):目录+图片+档案册同列、目录优先;副标题扫描期生成,
|
||||
* 不再有「· 续读 Pn」角标(角标随「目录自动当书」链路删除,续读仍由 reader 恢复);
|
||||
* A7 册间衔接根锁定(M18 评审 P2-1):相邻册越过浏览根时按「没有相邻册」处理,
|
||||
* 根内正常翻册不受影响(mr_book_adjacent_rooted)。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. test/test_cont_adjacent.c logic/library.c logic/reader.c \
|
||||
* test/settings_reader_stub.c -pthread -o test/test_cont_adjacent
|
||||
* 或 test/run_webtoon_tests.sh。
|
||||
*/
|
||||
#include "logic/library.h"
|
||||
#include "logic/reader.h"
|
||||
#include "test/settings_reader_stub.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int g_pass, g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
static int mkdir_p(const char *path)
|
||||
{
|
||||
char tmp[MR_PATH_MAX];
|
||||
snprintf(tmp, sizeof tmp, "%s", path);
|
||||
for (size_t i = 1; i <= strlen(tmp); i++) {
|
||||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||||
char ch = tmp[i];
|
||||
tmp[i] = '\0';
|
||||
if (mkdir(tmp, 0755) != 0) {
|
||||
struct stat st;
|
||||
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
|
||||
}
|
||||
tmp[i] = ch;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int touch_file(const char *path)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
int ok = fwrite("x", 1, 1, f) == 1;
|
||||
fclose(f);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
static int rm_rf(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) return -1;
|
||||
if (!S_ISDIR(st.st_mode)) return unlink(path);
|
||||
DIR *d = opendir(path);
|
||||
if (!d) return -1;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
|
||||
char full[MR_PATH_MAX];
|
||||
snprintf(full, sizeof full, "%s/%s", path, de->d_name);
|
||||
rm_rf(full);
|
||||
}
|
||||
closedir(d);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
static void joinp(char *out, int outsz, const char *a, const char *b)
|
||||
{
|
||||
snprintf(out, (size_t)outsz, "%s/%s", a, b);
|
||||
}
|
||||
|
||||
static void test_has_images(const char *root)
|
||||
{
|
||||
char p[MR_PATH_MAX];
|
||||
CHECK(mr_library_dir_has_images(root) == 1,
|
||||
"root has a loose image file (scan/adjacency only treats subdirs as books)");
|
||||
|
||||
joinp(p, sizeof p, root, "vol1");
|
||||
CHECK(mr_library_dir_has_images(p) == 1, "vol1 is a book");
|
||||
joinp(p, sizeof p, root, "dirs_only");
|
||||
CHECK(mr_library_dir_has_images(p) == 0, "dirs_only is not a book");
|
||||
joinp(p, sizeof p, root, "empty");
|
||||
CHECK(mr_library_dir_has_images(p) == 0, "empty is not a book");
|
||||
joinp(p, sizeof p, root, "txonly");
|
||||
CHECK(mr_library_dir_has_images(p) == 0, "text-only dir is not a book");
|
||||
CHECK(mr_library_dir_has_images("/no/such/dir/xyz") == 0, "missing dir");
|
||||
CHECK(mr_library_dir_has_images(NULL) == 0 && mr_library_dir_has_images("") == 0,
|
||||
"NULL/empty safe");
|
||||
}
|
||||
|
||||
static void expect_neighbor(const char *dir, int delta, const char *want, const char *tag)
|
||||
{
|
||||
char out[MR_PATH_MAX];
|
||||
int r = mr_book_adjacent(dir, delta, out, (int)sizeof out);
|
||||
if (!want) {
|
||||
CHECK(r == 0 && out[0] == '\0', "%s: no neighbor (r=%d out=%s)", tag, r, out);
|
||||
} else {
|
||||
CHECK(r == 1 && strcmp(out, want) == 0, "%s: got %s (want %s)", tag, out, want);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_adjacent(const char *root)
|
||||
{
|
||||
char vol1[MR_PATH_MAX], vol2[MR_PATH_MAX], vol10[MR_PATH_MAX];
|
||||
char series_a[MR_PATH_MAX], series_b[MR_PATH_MAX];
|
||||
char dirs_only[MR_PATH_MAX], out[MR_PATH_MAX];
|
||||
|
||||
joinp(vol1, sizeof vol1, root, "vol1");
|
||||
joinp(vol2, sizeof vol2, root, "vol2");
|
||||
joinp(vol10, sizeof vol10, root, "vol10");
|
||||
joinp(series_a, sizeof series_a, root, "series/a");
|
||||
joinp(series_b, sizeof series_b, root, "series/b");
|
||||
joinp(dirs_only, sizeof dirs_only, root, "dirs_only");
|
||||
|
||||
/* 自然序:vol1 < vol2 < vol10(vol0 是符号链接,不参与) */
|
||||
expect_neighbor(vol1, 1, vol2, "vol1 next");
|
||||
expect_neighbor(vol2, 1, vol10, "vol2 next");
|
||||
expect_neighbor(vol10, 1, NULL, "vol10 next (end)");
|
||||
expect_neighbor(vol1, -1, NULL, "vol1 prev (start)");
|
||||
expect_neighbor(vol2, -1, vol1, "vol2 prev");
|
||||
expect_neighbor(vol10, -1, vol2, "vol10 prev");
|
||||
|
||||
/* 末尾斜杠等价 */
|
||||
char slash[MR_PATH_MAX];
|
||||
snprintf(slash, sizeof slash, "%s/", vol2);
|
||||
expect_neighbor(slash, 1, vol10, "trailing slash next");
|
||||
expect_neighbor(slash, -1, vol1, "trailing slash prev");
|
||||
|
||||
/* 嵌套父目录:series/a <-> series/b */
|
||||
expect_neighbor(series_a, 1, series_b, "series a next");
|
||||
expect_neighbor(series_b, -1, series_a, "series b prev");
|
||||
expect_neighbor(series_b, 1, NULL, "series b next (end)");
|
||||
|
||||
/* 不是书的目录:不在兄弟表中 */
|
||||
expect_neighbor(dirs_only, 1, NULL, "dirs_only not a book");
|
||||
expect_neighbor(dirs_only, -1, NULL, "dirs_only not a book prev");
|
||||
|
||||
/* 路径参数:delta/缓冲/不存在 */
|
||||
CHECK(mr_book_adjacent(vol1, 0, out, (int)sizeof out) == -1, "delta 0 invalid");
|
||||
CHECK(mr_book_adjacent(vol1, 2, out, (int)sizeof out) == -1, "delta 2 invalid");
|
||||
CHECK(mr_book_adjacent(vol1, -2, out, (int)sizeof out) == -1, "delta -2 invalid");
|
||||
CHECK(mr_book_adjacent(NULL, 1, out, (int)sizeof out) == -1, "NULL dir");
|
||||
CHECK(mr_book_adjacent(vol1, 1, NULL, 32) == -1, "NULL out");
|
||||
CHECK(mr_book_adjacent(vol1, 1, out, 4) == 0, "small out -> no fit (0)");
|
||||
CHECK(mr_book_adjacent("/no/such/book", 1, out, (int)sizeof out) == 0,
|
||||
"missing parent -> 0");
|
||||
CHECK(mr_book_adjacent("/", 1, out, (int)sizeof out) == -1, "root path invalid");
|
||||
}
|
||||
|
||||
/* A6 文件管理器行模型:扫描即出最终行(无占位/无后台补位/无续读角标);
|
||||
* 目录优先;图片/档案册按类型成行。 */
|
||||
static void test_library_rows(const char *root)
|
||||
{
|
||||
mr_test_cfg_clear();
|
||||
char vol1[MR_PATH_MAX];
|
||||
joinp(vol1, sizeof vol1, root, "vol1");
|
||||
|
||||
/* vol1 有续读记录(第 5 页)——书库行不应出现角标。 */
|
||||
mr_cont_save(vol1, 4, MR_MODE_WEBTOON);
|
||||
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
|
||||
CHECK(n > 0, "library scan n=%d", n);
|
||||
if (n > 0)
|
||||
CHECK(lib.items[0].kind == MR_LIB_KIND_DIR, "dirs first: %s", lib.items[0].name);
|
||||
|
||||
int seen1 = 0, seen_img = 0;
|
||||
for (int i = 0; i < lib.count; i++) {
|
||||
const mr_library_entry *e = &lib.items[i];
|
||||
CHECK(e->subtitle[0] && strcmp(e->subtitle, "…") != 0,
|
||||
"%s subtitle pre-generated: %s", e->name, e->subtitle);
|
||||
CHECK(strstr(e->subtitle, "续读") == NULL, "%s has no badge: %s",
|
||||
e->name, e->subtitle);
|
||||
if (strcmp(e->name, "vol1") == 0) {
|
||||
seen1 = 1;
|
||||
CHECK(e->kind == MR_LIB_KIND_DIR, "vol1 is a dir row");
|
||||
}
|
||||
if (strcmp(e->name, "loose.png") == 0) {
|
||||
seen_img = 1;
|
||||
CHECK(e->kind == MR_LIB_KIND_IMAGE, "loose.png is an image row");
|
||||
}
|
||||
}
|
||||
CHECK(seen1 == 1, "vol1 listed");
|
||||
CHECK(seen_img == 1, "loose image listed");
|
||||
mr_library_free(&lib);
|
||||
}
|
||||
|
||||
/* A7 册间衔接的根锁定:相邻册必须在浏览根内,否则按「没有相邻册」处理。
|
||||
* 夹具:base/{aaa_outside, root}/{...};root 自己也是「册」(直接含图),
|
||||
* 明文 _ex 会给出 root 的兄弟 aaa_outside,rooted 必须拒绝(不越 SD 卡根)。 */
|
||||
static void test_rooted_adjacent(void)
|
||||
{
|
||||
char tmpl[] = "./mr_rooted_XXXXXX";
|
||||
char *base = mkdtemp(tmpl);
|
||||
CHECK(base != NULL, "mkdtemp rooted fixture");
|
||||
if (!base) return;
|
||||
|
||||
char root[MR_PATH_MAX], outside[MR_PATH_MAX], in1[MR_PATH_MAX], in2[MR_PATH_MAX];
|
||||
joinp(root, sizeof root, base, "root");
|
||||
joinp(outside, sizeof outside, base, "aaa_outside");
|
||||
joinp(in1, sizeof in1, root, "aaa_inside1");
|
||||
joinp(in2, sizeof in2, root, "aaa_inside2");
|
||||
CHECK(mkdir_p(outside) == 0 && mkdir_p(in1) == 0 && mkdir_p(in2) == 0,
|
||||
"mkdir rooted fixture");
|
||||
|
||||
char p[MR_PATH_MAX];
|
||||
joinp(p, sizeof p, root, "000.png"); CHECK(touch_file(p) == 0, "root image");
|
||||
joinp(p, sizeof p, outside, "001.png"); CHECK(touch_file(p) == 0, "outside image");
|
||||
joinp(p, sizeof p, in1, "001.png"); CHECK(touch_file(p) == 0, "inside1 image");
|
||||
joinp(p, sizeof p, in2, "001.png"); CHECK(touch_file(p) == 0, "inside2 image");
|
||||
|
||||
char out[MR_PATH_MAX];
|
||||
|
||||
/* 明文衔接:root 的兄弟 aaa_outside 在浏览根之外,_ex 会返回它。 */
|
||||
memset(out, 0, sizeof out);
|
||||
CHECK(mr_book_adjacent_ex(root, -1, out, (int)sizeof out, NULL, NULL) == 1 &&
|
||||
strcmp(out, outside) == 0,
|
||||
"plain adjacency reaches outside root (%s)", out);
|
||||
|
||||
/* 根锁定版:同样的调用必须拒绝越根。 */
|
||||
memset(out, 0x7f, sizeof out);
|
||||
out[0] = '\0';
|
||||
CHECK(mr_book_adjacent_rooted(root, -1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == 0 && out[0] == '\0',
|
||||
"rooted adjacency refuses book outside root (%s)", out);
|
||||
CHECK(mr_book_adjacent_rooted(root, 1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == 0 && out[0] == '\0',
|
||||
"rooted adjacency: no next inside root");
|
||||
|
||||
/* 根内正常翻册仍然可用(不能被误锁死)。 */
|
||||
memset(out, 0, sizeof out);
|
||||
CHECK(mr_book_adjacent_rooted(in1, 1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == 1 &&
|
||||
strcmp(out, in2) == 0, "rooted next inside root (%s)", out);
|
||||
CHECK(mr_book_adjacent_rooted(in2, -1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == 1 &&
|
||||
strcmp(out, in1) == 0, "rooted prev inside root (%s)", out);
|
||||
CHECK(mr_book_adjacent_rooted(in1, -1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == 0,
|
||||
"rooted: no book before inside1 (outside is out of root)");
|
||||
CHECK(mr_book_adjacent_rooted(in2, 1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == 0,
|
||||
"rooted: no book after inside2");
|
||||
|
||||
/* 当前册本身在根外 → 视为没有相邻册。 */
|
||||
CHECK(mr_book_adjacent_rooted(outside, 1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == 0, "current dir outside root");
|
||||
|
||||
/* 参数边界。 */
|
||||
CHECK(mr_book_adjacent_rooted(root, 1, NULL, NULL, NULL,
|
||||
out, (int)sizeof out) == -1, "rooted NULL root");
|
||||
CHECK(mr_book_adjacent_rooted(NULL, 1, root, NULL, NULL,
|
||||
out, (int)sizeof out) == -1, "rooted NULL dir");
|
||||
CHECK(mr_book_adjacent_rooted(root, 1, root, NULL, NULL, NULL, 32) == -1,
|
||||
"rooted NULL out");
|
||||
CHECK(mr_book_adjacent_rooted(root, 1, "", NULL, NULL,
|
||||
out, (int)sizeof out) == -1, "rooted empty root");
|
||||
|
||||
rm_rf(base);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
char tmpl[] = "./mr_cont_adj_XXXXXX";
|
||||
char *root = mkdtemp(tmpl);
|
||||
CHECK(root != NULL, "mkdtemp");
|
||||
if (!root) return 1;
|
||||
|
||||
char p[MR_PATH_MAX];
|
||||
const char *books[] = { "vol1", "vol2", "vol10", "series/a", "series/b", "dirs_only/inside" };
|
||||
for (unsigned i = 0; i < sizeof books / sizeof books[0]; i++) {
|
||||
joinp(p, sizeof p, root, books[i]);
|
||||
CHECK(mkdir_p(p) == 0, "mkdir %s", p);
|
||||
char img[MR_PATH_MAX];
|
||||
joinp(img, sizeof img, p, "001.png");
|
||||
CHECK(touch_file(img) == 0, "image %s", img);
|
||||
}
|
||||
joinp(p, sizeof p, root, "empty");
|
||||
mkdir_p(p);
|
||||
joinp(p, sizeof p, root, "txonly");
|
||||
mkdir_p(p);
|
||||
joinp(p, sizeof p, root, "txonly/readme.txt");
|
||||
touch_file(p);
|
||||
joinp(p, sizeof p, root, ".hidden");
|
||||
mkdir_p(p);
|
||||
joinp(p, sizeof p, root, ".hidden/001.png");
|
||||
touch_file(p);
|
||||
joinp(p, sizeof p, root, "MeowOS");
|
||||
mkdir_p(p);
|
||||
joinp(p, sizeof p, root, "MeowOS/001.png");
|
||||
touch_file(p);
|
||||
joinp(p, sizeof p, root, "loose.png");
|
||||
touch_file(p);
|
||||
|
||||
char vol1[MR_PATH_MAX], vol0[MR_PATH_MAX];
|
||||
joinp(vol1, sizeof vol1, root, "vol1");
|
||||
joinp(vol0, sizeof vol0, root, "vol0");
|
||||
if (symlink(vol1, vol0) == 0) { /* 有权限才测;无权限时 symlink 失败也不影响上面的断言 */ }
|
||||
|
||||
test_has_images(root);
|
||||
test_adjacent(root);
|
||||
test_library_rows(root);
|
||||
test_rooted_adjacent();
|
||||
|
||||
rm_rf(root);
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/* test_jump_page.c — 跳页状态机 + 条漫落位映射宿主测试(无 SDK/硬件)。
|
||||
*
|
||||
* 覆盖:
|
||||
* J1 跳页状态:begin 夹取/非法取消、active/target、预解码就绪落位、超时降级落位;
|
||||
* J2 落位映射:mr_strip_offset_for_page 与 page_at 互逆(首/中/末页,max 夹取);
|
||||
* J3 边界:空册/越界/重复取消;正常等待不提前落位(不白屏——没有图就不落)。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. test/test_jump_page.c logic/reader/nav.c \
|
||||
* -o test/test_jump_page && ./test/test_jump_page
|
||||
* 或 test/run_webtoon_tests.sh。
|
||||
*/
|
||||
#include "logic/reader/nav.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static int g_pass, g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
#define VW 480
|
||||
#define VH 720
|
||||
#define TIMEOUT 2500ULL
|
||||
|
||||
static int g_meta_w[8], g_meta_h[8], g_meta_n;
|
||||
|
||||
static int meta_cb(void *ctx, int i, int *w, int *h)
|
||||
{
|
||||
(void)ctx;
|
||||
if (i < 0 || i >= g_meta_n) return 0;
|
||||
*w = g_meta_w[i];
|
||||
*h = g_meta_h[i];
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void build_strip(mr_strip *s, int pages)
|
||||
{
|
||||
memset(s, 0, sizeof *s);
|
||||
g_meta_n = pages;
|
||||
for (int i = 0; i < pages; i++) {
|
||||
g_meta_w[i] = 800;
|
||||
g_meta_h[i] = 1200; /* 适宽后每页 720px */
|
||||
}
|
||||
CHECK(mr_strip_build(s, pages, VW, VH, meta_cb, NULL) == 0, "build strip pages=%d", pages);
|
||||
}
|
||||
|
||||
static void test_jump_state(void)
|
||||
{
|
||||
mr_jump j;
|
||||
memset(&j, 0, sizeof j);
|
||||
CHECK(mr_jump_active(&j) == 0, "fresh inactive");
|
||||
CHECK(mr_jump_target(&j) == -1, "fresh target -1");
|
||||
|
||||
/* 正常:开始 → 未就绪继续等 → 就绪落位 */
|
||||
mr_jump_begin(&j, 4, 10, 100);
|
||||
CHECK(mr_jump_active(&j) == 1 && mr_jump_target(&j) == 4, "begin active target=4");
|
||||
int to = -1;
|
||||
CHECK(mr_jump_poll(&j, 0, 200, TIMEOUT, &to) == 0 && to == 0, "waiting no timeout");
|
||||
CHECK(mr_jump_active(&j) == 1, "still active while waiting");
|
||||
CHECK(mr_jump_poll(&j, 1, 300, TIMEOUT, &to) == 1 && to == 0, "ready lands");
|
||||
CHECK(mr_jump_active(&j) == 0 && mr_jump_target(&j) == -1, "cleared after land");
|
||||
|
||||
/* 超时:仍然落位,但标记降级 */
|
||||
mr_jump_begin(&j, 2, 10, 1000);
|
||||
CHECK(mr_jump_poll(&j, 0, 1000 + TIMEOUT - 1, TIMEOUT, &to) == 0, "just before timeout waits");
|
||||
CHECK(mr_jump_poll(&j, 0, 1000 + TIMEOUT, TIMEOUT, &to) == 1 && to == 1, "timeout lands");
|
||||
CHECK(mr_jump_active(&j) == 0, "timeout clears");
|
||||
|
||||
/* 就绪与超时同时成立:优先算正常就绪 */
|
||||
mr_jump_begin(&j, 1, 10, 0);
|
||||
CHECK(mr_jump_poll(&j, 1, 999999, TIMEOUT, &to) == 1 && to == 0,
|
||||
"ready wins over timeout");
|
||||
|
||||
/* timeout=0 表示不限时 */
|
||||
mr_jump_begin(&j, 1, 10, 0);
|
||||
CHECK(mr_jump_poll(&j, 0, 999999999ULL, 0, &to) == 0 && to == 0, "no timeout when 0");
|
||||
|
||||
/* begin 夹取/非法 */
|
||||
mr_jump_begin(&j, -1, 10, 0);
|
||||
CHECK(mr_jump_active(&j) == 0, "begin(-1) cancels");
|
||||
mr_jump_begin(&j, 10, 10, 0);
|
||||
CHECK(mr_jump_active(&j) == 0, "begin(count) cancels");
|
||||
mr_jump_begin(&j, 0, 0, 0);
|
||||
CHECK(mr_jump_active(&j) == 0, "begin empty book cancels");
|
||||
|
||||
/* 取消幂等;NULL 安全 */
|
||||
mr_jump_begin(&j, 3, 10, 5);
|
||||
mr_jump_cancel(&j);
|
||||
mr_jump_cancel(&j);
|
||||
CHECK(mr_jump_active(&j) == 0, "cancel idempotent");
|
||||
CHECK(mr_jump_poll(NULL, 1, 1, TIMEOUT, &to) == 0, "poll NULL");
|
||||
mr_jump_cancel(NULL);
|
||||
mr_jump_begin(NULL, 0, 10, 0);
|
||||
|
||||
/* timed_out 可为 NULL */
|
||||
mr_jump_begin(&j, 5, 10, 0);
|
||||
CHECK(mr_jump_poll(&j, 0, TIMEOUT + 1, TIMEOUT, NULL) == 1, "poll NULL timed_out");
|
||||
}
|
||||
|
||||
static void test_land_mapping(void)
|
||||
{
|
||||
mr_strip s;
|
||||
build_strip(&s, 5);
|
||||
CHECK(mr_strip_total(&s) == 3600, "total=%d", mr_strip_total(&s));
|
||||
CHECK(mr_strip_max_offset(&s) == 2880, "max=%d", mr_strip_max_offset(&s));
|
||||
|
||||
int expect_top[5] = { 0, 720, 1440, 2160, 2880 };
|
||||
for (int i = 0; i < 5; i++) {
|
||||
int off = mr_strip_offset_for_page(&s, i);
|
||||
CHECK(off == expect_top[i], "page %d offset %d (want %d)", i, off, expect_top[i]);
|
||||
CHECK(mr_strip_page_at(&s, off) == i, "page_at(offset(%d)) == %d", i, i);
|
||||
}
|
||||
/* 末页顶端恰在 max(整页铺满视口),落位不会越界 */
|
||||
CHECK(mr_strip_offset_for_page(&s, 4) == mr_strip_max_offset(&s), "last page == max");
|
||||
/* page+1 的偏移等于 page 的末端(页间无缝) */
|
||||
CHECK(mr_strip_top(&s, 1) == mr_strip_top(&s, 0) + mr_strip_page_h(&s, 0),
|
||||
"page tops contiguous");
|
||||
|
||||
/* 跳页目标页必须在解码窗里(否则会被淘汰):窗口只给 1 槽时也能覆盖目标 */
|
||||
int f = -1, l = -1;
|
||||
CHECK(mr_strip_window(&s, 0, 1, &f, &l) == 1 && f == l, "single-slot window");
|
||||
mr_strip_free(&s);
|
||||
|
||||
/* 空册:没有可落位目标 */
|
||||
mr_strip e;
|
||||
memset(&e, 0, sizeof e);
|
||||
CHECK(mr_strip_offset_for_page(&e, 0) == -1, "empty book no offset");
|
||||
CHECK(mr_strip_page_at(&e, 0) == -1, "empty book no page");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_jump_state();
|
||||
test_land_mapping();
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/* test_library.c — 图库 logic 层宿主测试(纯 C,无 SDK/硬件)。
|
||||
*
|
||||
* 覆盖(M18 文件管理器模型):
|
||||
* T1 自然排序:数字段数值序(page2<page10)、前导零、数字/字母混合、大小写确定性;
|
||||
* T2 扩展名过滤:图片 png/jpg/jpeg/bmp 与档案册 cbz/zip 大小写不敏感;gif/txt 不支持;
|
||||
* T3 单层扫描:目录 + 受支持图片 + 档案册混排(目录优先、组内自然序);
|
||||
* 其它普通文件/隐藏项/符号链接不列;副标题扫描期生成一次(无占位/无懒判定);
|
||||
* 行类型;不可读路径 -> -1;max_depth 忽略;浏览根前缀判据 mr_path_within_root;
|
||||
* T4 阅读方向状态(logic/reader.c);
|
||||
* T5 目录内续读入口的页码判定(mr_cont_resume_page):无记录/空目录 -> 无入口,
|
||||
* 页数未知(列表截断)不夹持,保存页超页数钳到末页,记录按目录隔离。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. -I../sdk test/test_library.c logic/library.c logic/reader.c \
|
||||
* test/settings_reader_stub.c -pthread -o build/host/test_library
|
||||
* 或直接 test/run_tests.sh。
|
||||
*/
|
||||
#include "logic/library.h"
|
||||
#include "logic/reader.h"
|
||||
#include "test/settings_reader_stub.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int g_pass, g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
static int mkdir_p(const char *path)
|
||||
{
|
||||
char tmp[MR_PATH_MAX];
|
||||
snprintf(tmp, sizeof tmp, "%s", path);
|
||||
for (size_t i = 1; i <= strlen(tmp); i++) {
|
||||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||||
char ch = tmp[i];
|
||||
tmp[i] = '\0';
|
||||
if (mkdir(tmp, 0755) != 0) {
|
||||
struct stat st;
|
||||
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
|
||||
}
|
||||
tmp[i] = ch;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int touch_file(const char *path)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
fputs("x", f);
|
||||
return fclose(f);
|
||||
}
|
||||
|
||||
static int rm_rf(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) return -1;
|
||||
if (!S_ISDIR(st.st_mode)) return unlink(path);
|
||||
DIR *d = opendir(path);
|
||||
if (!d) return -1;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
|
||||
char full[MR_PATH_MAX];
|
||||
snprintf(full, sizeof full, "%s/%s", path, de->d_name);
|
||||
rm_rf(full);
|
||||
}
|
||||
closedir(d);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
static const mr_library_entry *find_entry(const mr_library *lib, const char *name)
|
||||
{
|
||||
for (int i = 0; i < lib->count; i++)
|
||||
if (strcmp(lib->items[i].name, name) == 0) return &lib->items[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void test_natural(void)
|
||||
{
|
||||
CHECK(mr_natural_cmp("page2", "page10") < 0, "page2 < page10");
|
||||
CHECK(mr_natural_cmp("page10", "page2") > 0, "page10 > page2");
|
||||
CHECK(mr_natural_cmp("page2", "page02") < 0, "fewer leading zeros first");
|
||||
CHECK(mr_natural_cmp("x001", "x1") > 0, "x1 < x001");
|
||||
CHECK(mr_natural_cmp("Chapter 9", "Chapter 10") < 0, "chapter 9 < 10");
|
||||
CHECK(mr_natural_cmp("img2a", "img10b") < 0, "mixed img2a < img10b");
|
||||
CHECK(mr_natural_cmp("a", "a1") < 0, "prefix shorter first");
|
||||
CHECK(mr_natural_cmp("abc", "abc") == 0, "identical equal");
|
||||
CHECK(mr_natural_cmp("ABC", "abc") != 0, "case-only tie is deterministic");
|
||||
CHECK(mr_natural_cmp("a.jpg", "A.jpg") > 0, "case tie falls back to raw bytes");
|
||||
CHECK(mr_natural_cmp("A2", "a10") < 0, "case-insensitive primary then numeric");
|
||||
}
|
||||
|
||||
static void test_ext(void)
|
||||
{
|
||||
CHECK(mr_path_is_image("a.png") == 1, "png");
|
||||
CHECK(mr_path_is_image("a.JPG") == 1, "JPG uppercase");
|
||||
CHECK(mr_path_is_image("a.jpeg") == 1, "jpeg");
|
||||
CHECK(mr_path_is_image("a.BmP") == 1, "BMP mixed case");
|
||||
CHECK(mr_path_is_image("a.gif") == 0, "gif unsupported");
|
||||
CHECK(mr_path_is_image("a.txt") == 0, "txt unsupported");
|
||||
CHECK(mr_path_is_image("png") == 0, "no extension");
|
||||
CHECK(mr_path_is_image(".png") == 0, "dotfile-ish no stem");
|
||||
CHECK(mr_path_is_archive("a.cbz") == 1, "cbz");
|
||||
CHECK(mr_path_is_archive("a.ZIP") == 1, "ZIP uppercase");
|
||||
CHECK(mr_path_is_archive("a.7z") == 0, "7z unsupported");
|
||||
CHECK(mr_path_is_archive("cbz") == 0, "no extension archive");
|
||||
}
|
||||
|
||||
static void test_parent(void)
|
||||
{
|
||||
char out[MR_PATH_MAX];
|
||||
CHECK(mr_path_parent("/usr/data/mnt/sd_0/a/b", out, sizeof out) == 0 &&
|
||||
strcmp(out, "/usr/data/mnt/sd_0/a") == 0, "parent nested");
|
||||
CHECK(mr_path_parent("/usr/data/mnt/sd_0/a/", out, sizeof out) == 0 &&
|
||||
strcmp(out, "/usr/data/mnt/sd_0") == 0, "parent trailing slash");
|
||||
CHECK(mr_path_parent("/a", out, sizeof out) == 0 && strcmp(out, "/") == 0, "parent root child");
|
||||
CHECK(mr_path_parent("/", out, sizeof out) == 0 && strcmp(out, "/") == 0, "parent root");
|
||||
}
|
||||
|
||||
static void test_scan(void)
|
||||
{
|
||||
char tmpl[] = "./mr_test_XXXXXX";
|
||||
char *root = mkdtemp(tmpl);
|
||||
CHECK(root != NULL, "mkdtemp");
|
||||
if (!root) return;
|
||||
|
||||
char p[MR_PATH_MAX];
|
||||
|
||||
snprintf(p, sizeof p, "%s/Series 10", root); mkdir_p(p);
|
||||
snprintf(p, sizeof p, "%s/Series 10/page1.png", root); touch_file(p);
|
||||
snprintf(p, sizeof p, "%s/Series 10/page2.jpg", root); touch_file(p);
|
||||
|
||||
snprintf(p, sizeof p, "%s/Series 2", root); mkdir_p(p);
|
||||
snprintf(p, sizeof p, "%s/Series 2/page10.png", root); touch_file(p);
|
||||
|
||||
snprintf(p, sizeof p, "%s/Series 02", root); mkdir_p(p);
|
||||
snprintf(p, sizeof p, "%s/Series 02/page1.bmp", root); touch_file(p);
|
||||
|
||||
snprintf(p, sizeof p, "%s/Series 3", root); mkdir_p(p);
|
||||
snprintf(p, sizeof p, "%s/Series 3/page1.gif", root); touch_file(p);
|
||||
|
||||
snprintf(p, sizeof p, "%s/Empty", root); mkdir_p(p);
|
||||
|
||||
snprintf(p, sizeof p, "%s/Nested/Deeper", root); mkdir_p(p);
|
||||
snprintf(p, sizeof p, "%s/Nested/Deeper/a.png", root); touch_file(p);
|
||||
|
||||
snprintf(p, sizeof p, "%s/.hidden", root); mkdir_p(p);
|
||||
snprintf(p, sizeof p, "%s/.hidden/h.png", root); touch_file(p);
|
||||
|
||||
snprintf(p, sizeof p, "%s/MeowAPPs", root); mkdir_p(p);
|
||||
snprintf(p, sizeof p, "%s/MeowAPPs/m.png", root); touch_file(p);
|
||||
|
||||
/* 当前层文件:图片与档案册都成行;txt/隐藏文件不成行。 */
|
||||
snprintf(p, sizeof p, "%s/root.png", root); touch_file(p);
|
||||
snprintf(p, sizeof p, "%s/cover.JPG", root); touch_file(p);
|
||||
snprintf(p, sizeof p, "%s/book.cbz", root); touch_file(p);
|
||||
snprintf(p, sizeof p, "%s/bundle.zip", root);touch_file(p);
|
||||
snprintf(p, sizeof p, "%s/notes.txt", root); touch_file(p);
|
||||
snprintf(p, sizeof p, "%s/.hidden.png", root); touch_file(p);
|
||||
|
||||
char link_path[MR_PATH_MAX], link_target[MR_PATH_MAX];
|
||||
snprintf(link_target, sizeof link_target, "%s/root.png", root);
|
||||
snprintf(link_path, sizeof link_path, "%s/link.png", root);
|
||||
symlink(link_target, link_path); /* 有权限才建;不列 */
|
||||
|
||||
mr_library lib;
|
||||
memset(&lib, 0, sizeof lib);
|
||||
int n = mr_library_scan(&lib, root, MR_LIB_MAX_DEPTH);
|
||||
CHECK(n == 11, "scan count=%d (want 11: 7 dirs + 4 files)", n);
|
||||
CHECK(lib.count == n, "count matches");
|
||||
CHECK(lib.truncated == 0, "not truncated");
|
||||
if (lib.count == 11) {
|
||||
static const char *want[11] = {
|
||||
"Empty", "MeowAPPs", "Nested", "Series 2", "Series 02", "Series 3", "Series 10",
|
||||
"book.cbz", "bundle.zip", "cover.JPG", "root.png"
|
||||
};
|
||||
for (int i = 0; i < 11; i++)
|
||||
CHECK(strcmp(lib.items[i].name, want[i]) == 0,
|
||||
"sort[%d]=%s got %s", i, want[i], lib.items[i].name);
|
||||
for (int i = 0; i < 7; i++)
|
||||
CHECK(lib.items[i].kind == MR_LIB_KIND_DIR, "dirs first: %s", lib.items[i].name);
|
||||
}
|
||||
|
||||
/* 行类型与副标题(扫描期生成一次,不是占位)。 */
|
||||
const mr_library_entry *e = find_entry(&lib, "Empty");
|
||||
CHECK(e && e->kind == MR_LIB_KIND_DIR && strcmp(e->subtitle, "文件夹") == 0,
|
||||
"dir subtitle=%s", e ? e->subtitle : "");
|
||||
e = find_entry(&lib, "root.png");
|
||||
CHECK(e && e->kind == MR_LIB_KIND_IMAGE && e->size == 1 &&
|
||||
strncmp(e->subtitle, "图片 · ", strlen("图片 · ")) == 0,
|
||||
"image subtitle=%s size=%lld", e ? e->subtitle : "", e ? e->size : -1);
|
||||
e = find_entry(&lib, "book.cbz");
|
||||
CHECK(e && e->kind == MR_LIB_KIND_ARCHIVE &&
|
||||
strncmp(e->subtitle, "档案册 · ", strlen("档案册 · ")) == 0,
|
||||
"archive subtitle=%s", e ? e->subtitle : "");
|
||||
|
||||
/* 排除项:普通文件/隐藏项/隐藏目录/符号链接。 */
|
||||
CHECK(find_entry(&lib, "notes.txt") == NULL, "plain text file excluded");
|
||||
CHECK(find_entry(&lib, ".hidden") == NULL, "hidden dir excluded");
|
||||
CHECK(find_entry(&lib, ".hidden.png") == NULL, "hidden file excluded");
|
||||
CHECK(find_entry(&lib, "link.png") == NULL, "symlink excluded");
|
||||
CHECK(find_entry(&lib, "a.png") == NULL, "no recursion into subdirs");
|
||||
CHECK(find_entry(&lib, "Deeper") == NULL, "no recursion into subdirs (deeper)");
|
||||
|
||||
/* 浏览根前缀判据(书库导航/阅读器册间衔接的根锁定;纯路径工具)。 */
|
||||
CHECK(mr_path_within_root("/usr/data/mnt/sd_0/a/b", "/usr/data/mnt/sd_0") == 1,
|
||||
"nested path inside root");
|
||||
CHECK(mr_path_within_root("/usr/data/mnt/sd_0", "/usr/data/mnt/sd_0") == 1,
|
||||
"root itself is inside");
|
||||
CHECK(mr_path_within_root("/usr/data/mnt/sd_0/", "/usr/data/mnt/sd_0") == 1,
|
||||
"root with trailing slash is inside");
|
||||
CHECK(mr_path_within_root("/usr/data/mnt/sd_0evil", "/usr/data/mnt/sd_0") == 0,
|
||||
"prefix-lookalike rejected");
|
||||
CHECK(mr_path_within_root("/usr/data/mnt/sd_00", "/usr/data/mnt/sd_0") == 0,
|
||||
"prefix-lookalike with digits rejected");
|
||||
CHECK(mr_path_within_root("/usr/data/mnt", "/usr/data/mnt/sd_0") == 0,
|
||||
"parent of root rejected");
|
||||
CHECK(mr_path_within_root("/usr/data/mnt/sd_0x/a", "/usr/data/mnt/sd_0") == 0,
|
||||
"sibling-prefixed path rejected");
|
||||
CHECK(mr_path_within_root("/a/b", "/a/") == 1, "root trailing slash tolerated");
|
||||
CHECK(mr_path_within_root("/a", "/") == 1, "filesystem root accepts abs path");
|
||||
CHECK(mr_path_within_root("/", "/") == 1, "filesystem root itself");
|
||||
CHECK(mr_path_within_root("rel/path", "/") == 0, "relative path not inside /");
|
||||
CHECK(mr_path_within_root(NULL, "/") == 0 && mr_path_within_root("/a", NULL) == 0 &&
|
||||
mr_path_within_root("/a", "") == 0, "NULL/empty safe");
|
||||
|
||||
/* 行合成:纯内存,返回扫描期生成的字符串。 */
|
||||
mr_library_row row;
|
||||
CHECK(mr_library_row_at(&lib, 0, &row) == 1 && row.index == 0 &&
|
||||
row.kind == MR_LIB_KIND_DIR && row.title && row.subtitle &&
|
||||
strcmp(row.subtitle, "文件夹") == 0, "row 0 = first dir");
|
||||
CHECK(mr_library_row_at(&lib, lib.count, &row) == 0, "no extra row when not truncated");
|
||||
CHECK(mr_library_row_at(&lib, -1, &row) == 0, "row -1 invalid");
|
||||
|
||||
/* max_depth 参数只为兼容旧调用保留:不再递归,结果一致。 */
|
||||
mr_library lib1;
|
||||
memset(&lib1, 0, sizeof lib1);
|
||||
CHECK(mr_library_scan(&lib1, root, 1) == 11, "max_depth ignored (single layer)");
|
||||
mr_library_free(&lib1);
|
||||
|
||||
mr_library_free(&lib);
|
||||
|
||||
/* 不可读目录 / 不是目录:返回 -1,lib 可安全 free。 */
|
||||
mr_library bad;
|
||||
memset(&bad, 0, sizeof bad);
|
||||
CHECK(mr_library_scan(&bad, "/no/such/dir/tuku", MR_LIB_MAX_DEPTH) == -1, "missing dir -> -1");
|
||||
CHECK(bad.count == 0, "missing dir empty");
|
||||
mr_library_free(&bad);
|
||||
|
||||
snprintf(p, sizeof p, "%s/root.png", root);
|
||||
memset(&bad, 0, sizeof bad);
|
||||
CHECK(mr_library_scan(&bad, p, MR_LIB_MAX_DEPTH) == -1, "file (not dir) -> -1");
|
||||
CHECK(bad.count == 0, "file scan empty");
|
||||
mr_library_free(&bad);
|
||||
|
||||
rm_rf(root);
|
||||
}
|
||||
|
||||
static void test_direction(void)
|
||||
{
|
||||
mr_reader_set_direction(MR_DIR_RTL);
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "default RTL");
|
||||
mr_reader_set_direction(MR_DIR_LTR);
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "switch LTR");
|
||||
mr_reader_set_direction(99);
|
||||
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "invalid ignored");
|
||||
mr_reader_set_direction(MR_DIR_RTL);
|
||||
}
|
||||
|
||||
/* 目录内「续读」入口的页码判定(M33)。 */
|
||||
static void test_cont_resume(void)
|
||||
{
|
||||
const char *dir = "/sdcard/album";
|
||||
const char *other = "/sdcard/other";
|
||||
mr_test_cfg_clear();
|
||||
|
||||
CHECK(mr_cont_resume_page(dir, 5) == -1, "no record -> no resume entry");
|
||||
|
||||
mr_cont_set_page(dir, 3);
|
||||
CHECK(mr_cont_resume_page(dir, 5) == 3, "page in range kept");
|
||||
CHECK(mr_cont_resume_page(dir, 4) == 3, "last valid page kept");
|
||||
CHECK(mr_cont_resume_page(dir, 0) == -1, "empty dir -> no resume entry");
|
||||
CHECK(mr_cont_resume_page(dir, -1) == 3,
|
||||
"unknown page count (truncated list) -> unclamped");
|
||||
|
||||
CHECK(mr_cont_resume_page(dir, 3) == 2, "page == pages -> clamp to last");
|
||||
mr_cont_set_page(dir, 99);
|
||||
CHECK(mr_cont_resume_page(dir, 3) == 2, "page beyond pages -> clamp to last");
|
||||
CHECK(mr_cont_resume_page(dir, 0) == -1, "stale record in empty dir -> no entry");
|
||||
|
||||
CHECK(mr_cont_resume_page(other, 9) == -1,
|
||||
"resume records are per directory (no leak)");
|
||||
CHECK(mr_cont_resume_page(NULL, 9) == -1, "NULL dir -> no entry");
|
||||
|
||||
/* 第一页也是有效记录(用户就停在第一页退出):入口要显示。 */
|
||||
mr_cont_set_page(dir, 0);
|
||||
CHECK(mr_cont_resume_page(dir, 1) == 0, "first page record is a valid entry");
|
||||
|
||||
mr_test_cfg_clear();
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_natural();
|
||||
test_ext();
|
||||
test_parent();
|
||||
test_scan();
|
||||
test_direction();
|
||||
test_cont_resume();
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/* test_mode_persist.c — 阅读模式 + 每册续读状态持久化宿主测试(无 SDK/硬件)。
|
||||
*
|
||||
* 覆盖:
|
||||
* P1 模式值:valid/name/parse(paged/webtoon,非法值回退);
|
||||
* P2 册标识键:reader.page/mode.<FNV1a64(规范路径)>,末尾斜杠等价、不同册不同键、
|
||||
* 键字符集安全([A-Za-z0-9._-]);
|
||||
* P3 默认值:无记录时 get_mode/get_page 返回调用方给的默认值;
|
||||
* P4 读写往返 + 册间隔离 + 一次 save 写两键;
|
||||
* P5 坏值/非法模式忽略(不覆盖已有记录);写失败只 WARN 不崩;
|
||||
* P6 书库角标:page>0 → "续读 Pn";page==0/无记录 → 无角标。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. test/test_mode_persist.c logic/reader.c \
|
||||
* test/settings_reader_stub.c -o test/test_mode_persist
|
||||
* 或 test/run_webtoon_tests.sh。
|
||||
*/
|
||||
#include "logic/reader.h"
|
||||
#include "test/settings_reader_stub.h"
|
||||
#include "include/meow_ui_config.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static int g_pass, g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
static void test_mode_values(void)
|
||||
{
|
||||
CHECK(mr_mode_valid(MR_MODE_PAGED) == 1, "paged valid");
|
||||
CHECK(mr_mode_valid(MR_MODE_WEBTOON) == 1, "webtoon valid");
|
||||
CHECK(mr_mode_valid(-1) == 0 && mr_mode_valid(2) == 0, "invalid modes");
|
||||
CHECK(strcmp(mr_mode_name(MR_MODE_PAGED), "paged") == 0, "name paged");
|
||||
CHECK(strcmp(mr_mode_name(MR_MODE_WEBTOON), "webtoon") == 0, "name webtoon");
|
||||
CHECK(strcmp(mr_mode_name(99), "paged") == 0, "name invalid -> paged");
|
||||
CHECK(mr_mode_parse("paged") == MR_MODE_PAGED, "parse paged");
|
||||
CHECK(mr_mode_parse("webtoon") == MR_MODE_WEBTOON, "parse webtoon");
|
||||
CHECK(mr_mode_parse("WEBTOON") == -1, "parse case sensitive");
|
||||
CHECK(mr_mode_parse("") == -1 && mr_mode_parse(NULL) == -1, "parse empty/NULL");
|
||||
}
|
||||
|
||||
static void test_keys(void)
|
||||
{
|
||||
char a[64], b[64], c[64];
|
||||
CHECK(mr_cont_key("/sd/book1", "page", a, (int)sizeof a) == 0, "key ok");
|
||||
CHECK(strncmp(a, "reader.page.", 12) == 0, "key prefix %s", a);
|
||||
CHECK(strlen(a) == 12 + 16, "key len=%zu", strlen(a));
|
||||
for (size_t i = 12; i < strlen(a); i++)
|
||||
CHECK(isxdigit((unsigned char)a[i]) != 0, "key hex char");
|
||||
CHECK(mr_cont_key("/sd/book1/", "page", b, (int)sizeof b) == 0, "key trailing slash");
|
||||
CHECK(strcmp(a, b) == 0, "trailing slash normalizes: %s vs %s", a, b);
|
||||
CHECK(mr_cont_key("/sd/book1", "mode", c, (int)sizeof c) == 0, "mode key");
|
||||
CHECK(strncmp(c, "reader.mode.", 12) == 0, "mode prefix %s", c);
|
||||
CHECK(strcmp(a, c) != 0, "field differs");
|
||||
CHECK(mr_cont_key("/sd/book2", "page", c, (int)sizeof c) == 0, "book2 key");
|
||||
CHECK(strcmp(a, c) != 0, "different books differ");
|
||||
|
||||
/* 参数/缓冲边界 */
|
||||
CHECK(mr_cont_key(NULL, "page", a, (int)sizeof a) == -1, "NULL dir");
|
||||
CHECK(mr_cont_key("", "page", a, (int)sizeof a) == -1, "empty dir");
|
||||
CHECK(mr_cont_key("/a", NULL, a, (int)sizeof a) == -1, "NULL field");
|
||||
CHECK(mr_cont_key("/a", "page", NULL, 32) == -1, "NULL out");
|
||||
CHECK(mr_cont_key("/a", "page", a, 8) == -1 && a[0] == '\0', "small out");
|
||||
}
|
||||
|
||||
static void test_defaults_and_roundtrip(void)
|
||||
{
|
||||
mr_test_cfg_clear();
|
||||
CHECK(mr_cont_get_mode("/sd/a", MR_MODE_PAGED) == MR_MODE_PAGED, "default paged");
|
||||
CHECK(mr_cont_get_mode("/sd/a", MR_MODE_WEBTOON) == MR_MODE_WEBTOON, "default param used");
|
||||
CHECK(mr_cont_get_page("/sd/a", 0) == 0, "default page 0");
|
||||
CHECK(mr_cont_get_page("/sd/a", 7) == 7, "default page param");
|
||||
CHECK(mr_cont_badge("/sd/a", NULL, 0) == 0, "badge NULL safe");
|
||||
|
||||
char badge[24];
|
||||
CHECK(mr_cont_badge("/sd/a", badge, (int)sizeof badge) == 0 && badge[0] == '\0',
|
||||
"no record -> no badge");
|
||||
|
||||
mr_cont_set_mode("/sd/a", MR_MODE_WEBTOON);
|
||||
CHECK(mr_cont_get_mode("/sd/a", MR_MODE_PAGED) == MR_MODE_WEBTOON, "mode roundtrip");
|
||||
CHECK(mr_cont_get_mode("/sd/b", MR_MODE_PAGED) == MR_MODE_PAGED, "mode isolated per book");
|
||||
|
||||
mr_cont_set_page("/sd/a", 17);
|
||||
CHECK(mr_cont_get_page("/sd/a", -1) == 17, "page roundtrip");
|
||||
CHECK(mr_cont_get_page("/sd/b", -1) == -1, "page isolated per book");
|
||||
CHECK(mr_cont_badge("/sd/a", badge, (int)sizeof badge) == 1, "badge shown");
|
||||
CHECK(strcmp(badge, "续读 P18") == 0, "badge text %s", badge);
|
||||
|
||||
/* 一次 save 两键(模式 + 页码) */
|
||||
mr_cont_save("/sd/c", 3, MR_MODE_WEBTOON);
|
||||
CHECK(mr_cont_get_page("/sd/c", -1) == 3, "save page");
|
||||
CHECK(mr_cont_get_mode("/sd/c", MR_MODE_PAGED) == MR_MODE_WEBTOON, "save mode");
|
||||
CHECK(mr_cont_badge("/sd/c", badge, (int)sizeof badge) == 1 &&
|
||||
strcmp(badge, "续读 P4") == 0, "save badge %s", badge);
|
||||
|
||||
/* page 0 视为「没有进度」,不出角标 */
|
||||
mr_cont_save("/sd/d", 0, MR_MODE_PAGED);
|
||||
CHECK(mr_cont_badge("/sd/d", badge, (int)sizeof badge) == 0 && badge[0] == '\0',
|
||||
"page 0 -> no badge");
|
||||
|
||||
/* NULL/空路径安全 */
|
||||
mr_cont_set_page(NULL, 5);
|
||||
mr_cont_set_mode(NULL, MR_MODE_WEBTOON);
|
||||
mr_cont_save(NULL, 1, 1);
|
||||
mr_cont_save("", 1, 1);
|
||||
CHECK(mr_cont_get_page(NULL, 9) == 9 && mr_cont_get_mode(NULL, MR_MODE_PAGED) == MR_MODE_PAGED,
|
||||
"NULL dir safe");
|
||||
}
|
||||
|
||||
static void test_bad_values(void)
|
||||
{
|
||||
mr_test_cfg_clear();
|
||||
char key[64];
|
||||
char buf[24];
|
||||
|
||||
/* 坏模式值 → 默认;坏页码(负数/非数字/超上限)→ 默认 */
|
||||
CHECK(mr_cont_key("/sd/x", "mode", key, (int)sizeof key) == 0, "bad key");
|
||||
mr_test_cfg_put(key, "vertical");
|
||||
CHECK(mr_cont_get_mode("/sd/x", MR_MODE_PAGED) == MR_MODE_PAGED, "bad mode -> def");
|
||||
|
||||
CHECK(mr_cont_key("/sd/x", "page", key, (int)sizeof key) == 0, "bad page key");
|
||||
mr_test_cfg_put(key, "-3");
|
||||
CHECK(mr_cont_get_page("/sd/x", 11) == 11, "negative page -> def");
|
||||
mr_test_cfg_put(key, "abc");
|
||||
CHECK(mr_cont_get_page("/sd/x", 12) == 12, "non-numeric page -> def");
|
||||
mr_test_cfg_put(key, "99999999");
|
||||
CHECK(mr_cont_get_page("/sd/x", 13) == 13, "too-large page -> def");
|
||||
mr_test_cfg_put(key, "0");
|
||||
CHECK(mr_cont_get_page("/sd/x", 14) == 0, "zero page readable");
|
||||
|
||||
/* 非法模式值不覆盖已有记录 */
|
||||
mr_cont_set_mode("/sd/y", MR_MODE_WEBTOON);
|
||||
mr_cont_set_mode("/sd/y", 42);
|
||||
CHECK(mr_cont_get_mode("/sd/y", MR_MODE_PAGED) == MR_MODE_WEBTOON, "invalid set ignored");
|
||||
|
||||
/* 写失败:只告警,内存保持旧值;不崩 */
|
||||
mr_cont_save("/sd/z", 5, MR_MODE_PAGED);
|
||||
int warns = mr_test_log_warns();
|
||||
mr_test_cfg_fail_writes(1);
|
||||
mr_cont_set_page("/sd/z", 9);
|
||||
mr_cont_set_mode("/sd/z", MR_MODE_WEBTOON);
|
||||
mr_cont_save("/sd/z", 10, MR_MODE_WEBTOON);
|
||||
CHECK(mr_test_log_warns() > warns, "write failure warns (warns=%d)", mr_test_log_warns());
|
||||
CHECK(mr_cont_get_page("/sd/z", -1) == 5, "failed write keeps old page");
|
||||
CHECK(mr_cont_get_mode("/sd/z", MR_MODE_PAGED) == MR_MODE_PAGED, "failed write keeps old mode");
|
||||
mr_test_cfg_fail_writes(0);
|
||||
|
||||
/* 写入后读回同一字符串(便于排查:paged/webtoon 明文) */
|
||||
CHECK(mr_cont_key("/sd/y", "mode", key, (int)sizeof key) == 0, "read mode key");
|
||||
CHECK(mr_test_cfg_get(key, buf, (int)sizeof buf) == 1 && strcmp(buf, "webtoon") == 0,
|
||||
"stored mode string %s", buf);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_mode_values();
|
||||
test_keys();
|
||||
test_defaults_and_roundtrip();
|
||||
test_bad_values();
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
/* test_mode_switch_host.c — 阅读器模式/续读/跳页/册间衔接的宿主集成测试(可选,需 SDK 源码树)。
|
||||
*
|
||||
* 只做纯逻辑测试覆盖不到的接缝:真实 SDK view/activity/widget + 真实图片解码 + 真实
|
||||
* meow_scroll/meow_anim 节拍。用 40x60 BMP 当页图(stb 可解,无外部依赖),在临时目录
|
||||
* 建两本相邻册,然后模拟用户操作:
|
||||
* H1 进入:默认翻页模式、页码 0、控件归属 ACT_READER(顶栏在 on_enter 建);
|
||||
* H2 chrome 图标一键切模式:翻页 ⇄ 条漫,切换立即写 meow_config(每册记忆);
|
||||
* H3 跳页滑块:展开 → 拖到末页 → 目标页解码完成后落位(不是先白屏再跳);
|
||||
* H4 离开/重进:恢复上次页 + 模式(续读);
|
||||
* H5 册间衔接:条漫末页上拉 → 下一册首页;首页下拉 → 上一册末页;首册再无上一册时
|
||||
* 停在原地(不退出、不崩);
|
||||
* H6 册间衔接根锁定:浏览根内没有相邻册时,不跨到根外的兄弟册(M18 评审观察项)。
|
||||
*
|
||||
* 编译:test/run_webtoon_tests.sh(需 SDK 源码树;只发布 meow_ui.a 时自动 SKIP)。
|
||||
*/
|
||||
#include "tuku_internal.h"
|
||||
#include "ui/reader/reader.h"
|
||||
#include "include/meow_ui_anim.h"
|
||||
#include "meow_ui_internal.h" /* MEOW_EV_TAP / MEOW_EV_SWIPE(宿主测试内部头) */
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { printf(" PASS\n"); } \
|
||||
else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
/* app.c 的导航上报在测试里不需要网络侧实现。 */
|
||||
void mr_nav_report(void) {}
|
||||
|
||||
static int g_lib_enters;
|
||||
static void lib_enter(meow_view *v, void *c) { (void)v; (void)c; g_lib_enters++; }
|
||||
static void lib_leave(meow_view *v, void *c) { (void)v; (void)c; }
|
||||
|
||||
/* ---- 临时目录 + 最小 BMP24(stb 可解) ---- */
|
||||
static void put16(unsigned char *p, unsigned v) { p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8); }
|
||||
static void put32(unsigned char *p, unsigned v)
|
||||
{
|
||||
p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8);
|
||||
p[2] = (unsigned char)(v >> 16); p[3] = (unsigned char)(v >> 24);
|
||||
}
|
||||
|
||||
static int write_bmp24(const char *path, int w, int h)
|
||||
{
|
||||
int stride = ((w * 3 + 3) / 4) * 4;
|
||||
int imgsz = stride * h;
|
||||
unsigned char fh[14] = { 0 }, ih[40] = { 0 };
|
||||
fh[0] = 'B'; fh[1] = 'M';
|
||||
put32(fh + 2, (unsigned)(54 + imgsz));
|
||||
put32(fh + 10, 54);
|
||||
put32(ih + 0, 40);
|
||||
put32(ih + 4, (unsigned)w);
|
||||
put32(ih + 8, (unsigned)h);
|
||||
put16(ih + 12, 1);
|
||||
put16(ih + 14, 24);
|
||||
put32(ih + 20, (unsigned)imgsz);
|
||||
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
int ok = fwrite(fh, 1, sizeof fh, f) == sizeof fh &&
|
||||
fwrite(ih, 1, sizeof ih, f) == sizeof ih;
|
||||
unsigned char *row = (unsigned char *)calloc(1, (size_t)stride);
|
||||
if (!row) ok = 0;
|
||||
for (int y = 0; ok && y < h; y++)
|
||||
if (fwrite(row, 1, (size_t)stride, f) != (size_t)stride) ok = 0;
|
||||
free(row);
|
||||
if (fclose(f) != 0) ok = 0;
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
static int mkdir_p(const char *path)
|
||||
{
|
||||
char tmp[MR_PATH_MAX];
|
||||
snprintf(tmp, sizeof tmp, "%s", path);
|
||||
for (size_t i = 1; i <= strlen(tmp); i++) {
|
||||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||||
char ch = tmp[i];
|
||||
tmp[i] = '\0';
|
||||
if (mkdir(tmp, 0755) != 0) {
|
||||
struct stat st;
|
||||
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
|
||||
}
|
||||
tmp[i] = ch;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int rm_rf(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) return -1;
|
||||
if (!S_ISDIR(st.st_mode)) return unlink(path);
|
||||
DIR *d = opendir(path);
|
||||
if (!d) return -1;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
|
||||
char full[MR_PATH_MAX];
|
||||
snprintf(full, sizeof full, "%s/%s", path, de->d_name);
|
||||
rm_rf(full);
|
||||
}
|
||||
closedir(d);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
/* 驱动 SDK 动画节拍(宿主没有 meow_ui_run 主循环)。 */
|
||||
static void pump_ms(int ms)
|
||||
{
|
||||
for (int i = 0; i < ms / 10; i++) {
|
||||
usleep(10 * 1000);
|
||||
meow_anim_tick();
|
||||
}
|
||||
}
|
||||
|
||||
/* 模拟 app.c 的点击路由:on_touch_down → on_tap(阅读页 tap 不在 view on_event 里)。 */
|
||||
static int tap(int x, int y)
|
||||
{
|
||||
reader_on_touch_down(x, y);
|
||||
reader_on_tap(x, y);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int swipe(int dir, int dist)
|
||||
{
|
||||
return meow_view_dispatch_event(MEOW_EV_SWIPE, dir, dist);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const char *base = getenv("TMPDIR");
|
||||
if (!base || !base[0]) base = ".";
|
||||
char tmpl[512];
|
||||
snprintf(tmpl, sizeof tmpl, "%s/mr_mode_host_XXXXXX", base);
|
||||
char *data = mkdtemp(tmpl);
|
||||
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
|
||||
setenv("MEOW_DATA_DIR", data, 1);
|
||||
|
||||
char root[MR_PATH_MAX], book1[MR_PATH_MAX], book2[MR_PATH_MAX];
|
||||
snprintf(root, sizeof root, "./mr_mode_books_%ld", (long)getpid());
|
||||
snprintf(book1, sizeof book1, "%s/vol1", root);
|
||||
snprintf(book2, sizeof book2, "%s/vol2", root);
|
||||
mkdir_p(book1);
|
||||
mkdir_p(book2);
|
||||
for (int b = 0; b < 2; b++) {
|
||||
const char *dir = b ? book2 : book1;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
char p[MR_PATH_MAX];
|
||||
snprintf(p, sizeof p, "%s/p%d.bmp", dir, i + 1);
|
||||
if (write_bmp24(p, 40, 60) != 0) { printf("SKIP: bmp write failed\n"); return 0; }
|
||||
}
|
||||
}
|
||||
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
|
||||
|
||||
static const meow_view_hooks lh = { .on_enter = lib_enter, .on_leave = lib_leave };
|
||||
meow_view *lib = meow_view_create(ACT_LIBRARY, MEOW_VIEW_ACTIVITY, &lh, NULL);
|
||||
meow_view *reader = reader_view_create();
|
||||
meow_view_page_push(ACT_LIBRARY);
|
||||
|
||||
/* 阅读器册间衔接的浏览根:测试夹具根(生产默认 MR_ROOT_DIR)。 */
|
||||
mr_reader_test_set_root(root);
|
||||
CHECK(strcmp(mr_reader_test_root(), root) == 0, "reader browse root installed");
|
||||
|
||||
/* TopAppBar 动作格(位图):右起每格 48px;动作 0=模式 / 1=跳页 / 2=fit。 */
|
||||
int w = meow_ui_width();
|
||||
int mode_x = w - 10 - 48 * 3 + 24;
|
||||
int jump_x = w - 10 - 48 * 2 + 24;
|
||||
int bar_y = 28;
|
||||
int slider_y = 56 + 4 + 20;
|
||||
int sx = 24, sw = w - 48;
|
||||
|
||||
/* ---- H1 进入:翻页模式、第 1 页 ---- */
|
||||
mr_reader_open_dir(book1);
|
||||
CHECK(meow_ui_activity_active() == ACT_READER, "reader active after open");
|
||||
CHECK(mr_reader_ui_count() == 3, "book pages=%d", mr_reader_ui_count());
|
||||
CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "default mode is paged");
|
||||
CHECK(mr_reader_ui_page() == 0, "start at page 0");
|
||||
pump_ms(300);
|
||||
|
||||
/* ---- H2 模式切换:chrome 图标一键切,立即持久化 ---- */
|
||||
CHECK(tap(mode_x, bar_y) == 1, "mode icon tap consumed");
|
||||
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "switched to webtoon");
|
||||
CHECK(mr_cont_get_mode(book1, MR_MODE_PAGED) == MR_MODE_WEBTOON,
|
||||
"webtoon mode persisted per book");
|
||||
CHECK(tap(mode_x, bar_y) == 1, "mode icon tap consumed (back)");
|
||||
CHECK(mr_reader_ui_mode() == MR_MODE_PAGED, "switched back to paged");
|
||||
CHECK(mr_cont_get_mode(book1, MR_MODE_PAGED) == MR_MODE_PAGED, "paged mode persisted");
|
||||
CHECK(tap(mode_x, bar_y) == 1, "mode icon tap consumed (again)");
|
||||
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon again for continuity");
|
||||
|
||||
/* ---- H3 跳页:滑块展开 → 拖到末页 → 预解码完成后落位 ---- */
|
||||
CHECK(tap(jump_x, bar_y) == 1, "jump icon tap consumed");
|
||||
CHECK(mr_reader_ui_jump_open() == 1, "slider open");
|
||||
reader_on_touch_down(sx + sw, slider_y);
|
||||
reader_on_tap(sx + sw, slider_y);
|
||||
CHECK(mr_reader_ui_jump_open() == 0, "slider closed after pick");
|
||||
CHECK(mr_reader_ui_jump_pending() == 1 || mr_reader_ui_page() == 2,
|
||||
"jump pending (waiting for target decode)");
|
||||
pump_ms(600);
|
||||
CHECK(mr_reader_ui_jump_pending() == 0, "jump landed");
|
||||
CHECK(mr_reader_ui_page() == 2, "jumped to last page (got %d)", mr_reader_ui_page());
|
||||
|
||||
/* ---- H4 续读:离开保存 page+mode,重进恢复 ---- */
|
||||
CHECK(swipe(MEOW_SWIPE_LEFT, 200) == 0, "reader leaves back-swipe to app.c");
|
||||
meow_view_page_pop();
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library");
|
||||
CHECK(mr_cont_get_page(book1, -1) == 2, "last page saved (got %d)",
|
||||
mr_cont_get_page(book1, -1));
|
||||
CHECK(mr_cont_get_mode(book1, MR_MODE_PAGED) == MR_MODE_WEBTOON, "mode saved");
|
||||
CHECK(g_lib_enters >= 2, "library re-entered");
|
||||
|
||||
mr_reader_open_dir(book1);
|
||||
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "mode restored on reopen");
|
||||
CHECK(mr_reader_ui_page() == 2, "page restored on reopen (got %d)", mr_reader_ui_page());
|
||||
pump_ms(300);
|
||||
|
||||
/* ---- H5 册间衔接:条漫末页上拉 → 下一册首页 ---- */
|
||||
reader_on_touch_down(240, 600);
|
||||
for (int i = 1; i <= 6; i++) {
|
||||
reader_on_touch_move(240, 600 - i * 40);
|
||||
usleep(5 * 1000);
|
||||
}
|
||||
CHECK(swipe(MEOW_SWIPE_UP, 240) == 1, "webtoon up-swipe consumed");
|
||||
CHECK(strcmp(mr_reader_dir(), book2) == 0, "advanced to next book (got %s)", mr_reader_dir());
|
||||
CHECK(mr_reader_ui_page() == 0, "next book starts at page 0");
|
||||
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "mode carried into fresh book");
|
||||
pump_ms(300);
|
||||
|
||||
/* 首页下拉 → 上一册末页 */
|
||||
reader_on_touch_down(240, 300);
|
||||
for (int i = 1; i <= 6; i++) {
|
||||
reader_on_touch_move(240, 300 + i * 40);
|
||||
usleep(5 * 1000);
|
||||
}
|
||||
CHECK(swipe(MEOW_SWIPE_DOWN, 240) == 1, "webtoon down-swipe consumed");
|
||||
CHECK(strcmp(mr_reader_dir(), book1) == 0, "back to previous book (got %s)", mr_reader_dir());
|
||||
CHECK(mr_reader_ui_page() == 2, "previous book lands at last page (got %d)",
|
||||
mr_reader_ui_page());
|
||||
pump_ms(300);
|
||||
|
||||
/* 跳到首页后,再往上一册:没有相邻册,停在原地并提示(不退出/不崩)。 */
|
||||
CHECK(tap(jump_x, bar_y) == 1, "jump icon tap consumed (to first page)");
|
||||
CHECK(mr_reader_ui_jump_open() == 1, "slider open again");
|
||||
reader_on_touch_down(sx + 2, slider_y);
|
||||
reader_on_tap(sx + 2, slider_y);
|
||||
pump_ms(500);
|
||||
CHECK(mr_reader_ui_page() == 0, "jumped to first page (got %d)", mr_reader_ui_page());
|
||||
|
||||
reader_on_touch_down(240, 300);
|
||||
for (int i = 1; i <= 6; i++) {
|
||||
reader_on_touch_move(240, 300 + i * 40);
|
||||
usleep(5 * 1000);
|
||||
}
|
||||
CHECK(swipe(MEOW_SWIPE_DOWN, 240) == 1, "edge down-swipe consumed");
|
||||
CHECK(strcmp(mr_reader_dir(), book1) == 0, "no previous book: stay put");
|
||||
CHECK(meow_ui_activity_active() == ACT_READER, "no previous book: still in reader (no exit)");
|
||||
|
||||
/* ---- H6 册间衔接根锁定:浏览根=book2 时,兄弟册 book1 在根外,不得翻过去 ---- */
|
||||
meow_view_page_pop();
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library for root-lock case");
|
||||
mr_reader_test_set_root(book2);
|
||||
CHECK(strcmp(mr_reader_test_root(), book2) == 0, "reader root narrowed to book2");
|
||||
mr_reader_open_dir_at(book2, 0); /* 显式首页,避开续读页差异 */
|
||||
CHECK(meow_ui_activity_active() == ACT_READER, "reopened book2");
|
||||
CHECK(strcmp(mr_reader_dir(), book2) == 0, "at book2");
|
||||
if (mr_reader_ui_mode() != MR_MODE_WEBTOON) mr_reader_test_toggle_mode();
|
||||
CHECK(mr_reader_ui_mode() == MR_MODE_WEBTOON, "webtoon for root-lock case");
|
||||
CHECK(mr_reader_ui_page() == 0, "root-lock case at first page (got %d)",
|
||||
mr_reader_ui_page());
|
||||
|
||||
/* 首页下拉 → book1 是 book2 的上一册,但在浏览根外:必须原地不动。 */
|
||||
reader_on_touch_down(240, 300);
|
||||
for (int i = 1; i <= 6; i++) {
|
||||
reader_on_touch_move(240, 300 + i * 40);
|
||||
usleep(5 * 1000);
|
||||
}
|
||||
CHECK(swipe(MEOW_SWIPE_DOWN, 240) == 1, "root-lock down-swipe consumed");
|
||||
CHECK(strcmp(mr_reader_dir(), book2) == 0,
|
||||
"adjacent book outside browse root is refused (dir=%s)", mr_reader_dir());
|
||||
CHECK(meow_ui_activity_active() == ACT_READER, "root-lock: still in reader");
|
||||
mr_reader_test_set_root(root); /* 复原夹具根 */
|
||||
|
||||
/* ---- 收尾 ---- */
|
||||
meow_view_page_pop();
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "popped back to library");
|
||||
meow_view_free(reader);
|
||||
meow_view_free(lib);
|
||||
meow_ui_shutdown();
|
||||
|
||||
rm_rf(root);
|
||||
rm_rf(data);
|
||||
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
|
||||
g_fail ? "see failures above" : "reader mode/continuity integration checks passed");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
/* test_open_book_host.c — 书库点击 → 阅读器打开的宿主集成回归(需 SDK 源码树)。
|
||||
*
|
||||
* M18 文件管理器模型:点目录=进入;点图片=从该图开始读(mr_reader_open_dir_at(目录, 下标));
|
||||
* 点档案册=打开档案册。本测试用合成夹具(真实 BMP,不读用户相册)走完整链路:
|
||||
* library 列表 → 点图片行 → mr_reader_open_dir_at → reader on_enter → mr_book_open_auto
|
||||
* 并断言「点哪张就开哪张」:阅读器当前页 == 该书库行在目录图片自然序里的下标。
|
||||
*
|
||||
* 另覆盖:
|
||||
* - 目录行只进目录,不打开阅读器;
|
||||
* - 阅读页已在本页时再次 mr_reader_open_dir_at 能换册/换页重进(SDK 忽略重复 push 的坑);
|
||||
* - 离开阅读器回书库后再次点图(同目录二次打开)。
|
||||
*
|
||||
* 编译:见 test/run_tests.sh(与 test_mode_switch_host 同口径)。
|
||||
*/
|
||||
#include "tuku_internal.h"
|
||||
#include "ui/reader/reader.h"
|
||||
#include "logic/reader/page_source.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* ui/library/library.c 的测试接缝 */
|
||||
int library_test_load(const char *dir);
|
||||
void library_test_set_root(const char *root);
|
||||
const char *library_test_dir(void);
|
||||
int library_test_rows(void);
|
||||
int library_test_src(int idx, meow_list_item *item);
|
||||
int library_test_open_row(int idx);
|
||||
|
||||
/* ui/reader/reader.c 的测试接缝(-DMR_READER_HOST_TEST) */
|
||||
int mr_reader_ui_mode(void);
|
||||
int mr_reader_ui_page(void);
|
||||
int mr_reader_ui_count(void);
|
||||
int mr_reader_ui_open_err(void);
|
||||
void mr_reader_open_dir_at_name(const char *dir, const char *name);
|
||||
|
||||
static int g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { printf(" PASS\n"); } \
|
||||
else { g_fail++; printf(" FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
void mr_nav_report(void) {}
|
||||
|
||||
static void put16(unsigned char *p, unsigned v)
|
||||
{
|
||||
p[0] = (unsigned char)v;
|
||||
p[1] = (unsigned char)(v >> 8);
|
||||
}
|
||||
static void put32(unsigned char *p, unsigned v)
|
||||
{
|
||||
p[0] = (unsigned char)v; p[1] = (unsigned char)(v >> 8);
|
||||
p[2] = (unsigned char)(v >> 16); p[3] = (unsigned char)(v >> 24);
|
||||
}
|
||||
|
||||
static int write_bmp24(const char *path, int w, int h)
|
||||
{
|
||||
int stride = ((w * 3 + 3) / 4) * 4;
|
||||
int imgsz = stride * h;
|
||||
unsigned char fh[14] = { 0 }, ih[40] = { 0 };
|
||||
fh[0] = 'B'; fh[1] = 'M';
|
||||
put32(fh + 2, (unsigned)(54 + imgsz));
|
||||
put32(fh + 10, 54);
|
||||
put32(ih + 0, 40);
|
||||
put32(ih + 4, (unsigned)w);
|
||||
put32(ih + 8, (unsigned)h);
|
||||
put16(ih + 12, 1);
|
||||
put16(ih + 14, 24);
|
||||
put32(ih + 20, (unsigned)imgsz);
|
||||
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
int ok = fwrite(fh, 1, sizeof fh, f) == sizeof fh &&
|
||||
fwrite(ih, 1, sizeof ih, f) == sizeof ih;
|
||||
unsigned char *row = (unsigned char *)calloc(1, (size_t)stride);
|
||||
if (!row) ok = 0;
|
||||
for (int y = 0; ok && y < h; y++)
|
||||
if (fwrite(row, 1, (size_t)stride, f) != (size_t)stride) ok = 0;
|
||||
free(row);
|
||||
if (fclose(f) != 0) ok = 0;
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
static int mkdir_p(const char *path)
|
||||
{
|
||||
char tmp[MR_PATH_MAX];
|
||||
snprintf(tmp, sizeof tmp, "%s", path);
|
||||
for (size_t i = 1; i <= strlen(tmp); i++) {
|
||||
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||||
char ch = tmp[i];
|
||||
tmp[i] = '\0';
|
||||
if (mkdir(tmp, 0755) != 0) {
|
||||
struct stat st;
|
||||
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
|
||||
}
|
||||
tmp[i] = ch;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int rm_rf(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path, &st) != 0) return -1;
|
||||
if (!S_ISDIR(st.st_mode)) return unlink(path);
|
||||
DIR *d = opendir(path);
|
||||
if (!d) return -1;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
|
||||
char full[MR_PATH_MAX];
|
||||
snprintf(full, sizeof full, "%s/%s", path, de->d_name);
|
||||
rm_rf(full);
|
||||
}
|
||||
closedir(d);
|
||||
return rmdir(path);
|
||||
}
|
||||
|
||||
static int row_index_for(const char *name)
|
||||
{
|
||||
int rows = library_test_rows();
|
||||
for (int i = 0; i < rows; i++) {
|
||||
meow_list_item it;
|
||||
if (!library_test_src(i, &it)) continue;
|
||||
if (it.title && strcmp(it.title, name) == 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 用真实点击坐标点某一行(列表顶端 = 状态栏 + 56px 顶栏,行高 56)。 */
|
||||
static void tap_row(const char *name)
|
||||
{
|
||||
int idx = row_index_for(name);
|
||||
if (idx < 0) { CHECK(0, "row %s not found", name); return; }
|
||||
int list_top = meow_ui_statusbar_height() + 56;
|
||||
library_on_tap(120, list_top + idx * 56 + 28);
|
||||
}
|
||||
|
||||
/* 某张图片行在本目录图片自然序里的下标 = 排在前面的「图片 · 」行数。 */
|
||||
static int expected_image_index(const char *name)
|
||||
{
|
||||
int ti = row_index_for(name);
|
||||
if (ti < 0) return -1;
|
||||
int n = 0;
|
||||
for (int i = 0; i < ti; i++) {
|
||||
meow_list_item it;
|
||||
if (!library_test_src(i, &it)) continue;
|
||||
if (it.subtitle && strncmp(it.subtitle, "图片 · ", strlen("图片 · ")) == 0) n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* 阅读器目录册用的假解码回调(本测试不经过它们取页)。 */
|
||||
static meow_img *cap_fake_load(const char *path, void *ud, int maxdim)
|
||||
{
|
||||
(void)path; (void)ud; (void)maxdim;
|
||||
return NULL;
|
||||
}
|
||||
static void cap_fake_unload(meow_img *img) { (void)img; }
|
||||
|
||||
/* 在阅读器的全量目录册里按文件名取页下标(与阅读器打开时同一份枚举)。 */
|
||||
static int reader_page_of_name(const mr_dir_book *db, const char *name)
|
||||
{
|
||||
for (int i = 0; i < db->count; i++)
|
||||
if (db->names[i] && strcmp(db->names[i], name) == 0) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const char *base = getenv("TMPDIR");
|
||||
if (!base || !base[0]) base = ".";
|
||||
char tmpl[512];
|
||||
snprintf(tmpl, sizeof tmpl, "%s/mr_open_host_XXXXXX", base);
|
||||
char *data = mkdtemp(tmpl);
|
||||
if (!data) { printf("SKIP: mkdtemp failed\n"); return 0; }
|
||||
setenv("MEOW_DATA_DIR", data, 1);
|
||||
|
||||
char root[MR_PATH_MAX], pics[MR_PATH_MAX], other[MR_PATH_MAX], p[MR_PATH_MAX];
|
||||
snprintf(root, sizeof root, "%s/library", data);
|
||||
snprintf(pics, sizeof pics, "%s/Pictures", root);
|
||||
snprintf(other, sizeof other, "%s/Other", root);
|
||||
if (mkdir_p(pics) != 0 || mkdir_p(other) != 0) { printf("SKIP: mkdir failed\n"); return 0; }
|
||||
|
||||
/* Pictures:6 张图(数字序号/大小写/前导零混排)+ 1 个普通文件(不成行)。 */
|
||||
const char *imgs[] = { "A.bmp", "a.bmp", "img2.bmp", "img10.bmp",
|
||||
"page3.bmp", "Page03.bmp" };
|
||||
for (unsigned i = 0; i < sizeof imgs / sizeof imgs[0]; i++) {
|
||||
snprintf(p, sizeof p, "%s/%s", pics, imgs[i]);
|
||||
if (write_bmp24(p, 40, 60) != 0) { printf("SKIP: image write failed\n"); return 0; }
|
||||
}
|
||||
snprintf(p, sizeof p, "%s/notes.txt", pics);
|
||||
{ FILE *f = fopen(p, "wb"); if (f) { fputs("x", f); fclose(f); } }
|
||||
|
||||
/* Other:4 张图,用于「已在本页时换册重进」。 */
|
||||
for (int i = 0; i < 4; i++) {
|
||||
snprintf(p, sizeof p, "%s/o%d.bmp", other, i + 1);
|
||||
if (write_bmp24(p, 40, 60) != 0) { printf("SKIP: image write failed\n"); return 0; }
|
||||
}
|
||||
|
||||
/* Big:2100 张图(超过默认 MR_LIB_SCAN_MAX=2000,复现评审 P1-1 截断错位场景)。
|
||||
* 1x1 BMP(stb 可解)以控制夹具体积/建盘时间。 */
|
||||
char big[MR_PATH_MAX];
|
||||
snprintf(big, sizeof big, "%s/Big", root);
|
||||
if (mkdir_p(big) != 0) { printf("SKIP: mkdir Big failed\n"); return 0; }
|
||||
for (int i = 1; i <= 2100; i++) {
|
||||
snprintf(p, sizeof p, "%s/p%04d.bmp", big, i);
|
||||
if (write_bmp24(p, 1, 1) != 0) { printf("SKIP: big image write failed\n"); return 0; }
|
||||
}
|
||||
|
||||
meow_ui_opts o = { 480, 720 };
|
||||
if (meow_ui_init(&o) != 0) { printf("SKIP: meow_ui_init failed (no host backend)\n"); return 0; }
|
||||
|
||||
library_test_set_root(root);
|
||||
meow_view *lib = library_view_create();
|
||||
meow_view *reader = reader_view_create();
|
||||
meow_view_page_push(ACT_LIBRARY);
|
||||
|
||||
CHECK(library_test_load(root) == 1, "load fixture root");
|
||||
|
||||
/* 点目录 = 进入目录,不打开阅读器。 */
|
||||
tap_row("Pictures");
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "tap dir stays in library");
|
||||
CHECK(strcmp(library_test_dir(), pics) == 0, "entered Pictures (%s)", library_test_dir());
|
||||
CHECK(library_test_rows() == 6, "Pictures rows=%d (6 images, txt excluded)",
|
||||
library_test_rows());
|
||||
|
||||
/* 点 img10.bmp:阅读器从该图开始(count=6, page=它在该目录的自然序下标)。 */
|
||||
int want = expected_image_index("img10.bmp");
|
||||
CHECK(want == 3, "img10.bmp natural image index=%d (want 3)", want);
|
||||
tap_row("img10.bmp");
|
||||
CHECK(meow_ui_activity_active() == ACT_READER, "tap image opens reader");
|
||||
CHECK(strcmp(mr_reader_dir(), pics) == 0, "reader dir=%s", mr_reader_dir());
|
||||
CHECK(mr_reader_ui_open_err() == 0, "open has no error");
|
||||
CHECK(mr_reader_ui_count() == 6, "reader pages=%d (want 6)", mr_reader_ui_count());
|
||||
CHECK(mr_reader_ui_page() == want, "reader starts at selected image: page=%d (want %d)",
|
||||
mr_reader_ui_page(), want);
|
||||
|
||||
/* 让首帧解码 tick 跑一遍,确认不会因坏夹具进入 open_err。 */
|
||||
for (int i = 0; i < 20; i++) usleep(10 * 1000);
|
||||
CHECK(mr_reader_ui_open_err() == 0, "still no open_err after decode tick");
|
||||
|
||||
/* 已在本页:直接换册重进(M17 遗留修复,SDK 会忽略重复 push)。 */
|
||||
mr_reader_open_dir_at(other, 2);
|
||||
CHECK(meow_ui_activity_active() == ACT_READER, "switch keeps reader active");
|
||||
CHECK(strcmp(mr_reader_dir(), other) == 0, "switched reader dir=%s", mr_reader_dir());
|
||||
CHECK(mr_reader_ui_count() == 4, "switched book pages=%d (want 4)", mr_reader_ui_count());
|
||||
CHECK(mr_reader_ui_page() == 2, "switched book page=%d (want 2)", mr_reader_ui_page());
|
||||
|
||||
/* 返回书库 → 再点另一张图(同目录二次打开 + 显式起始页优先于续读)。 */
|
||||
meow_view_page_pop();
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library");
|
||||
CHECK(strcmp(library_test_dir(), pics) == 0, "library still in Pictures");
|
||||
|
||||
tap_row("page3.bmp");
|
||||
CHECK(meow_ui_activity_active() == ACT_READER, "reopen reader");
|
||||
CHECK(mr_reader_ui_open_err() == 0, "reopen no error");
|
||||
CHECK(mr_reader_ui_count() == 6, "reopen pages=%d", mr_reader_ui_count());
|
||||
int want2 = expected_image_index("page3.bmp");
|
||||
CHECK(want2 == 4, "page3.bmp natural image index=%d (want 4)", want2);
|
||||
CHECK(mr_reader_ui_page() == want2, "reopen starts at page=%d (want %d)",
|
||||
mr_reader_ui_page(), want2);
|
||||
meow_view_page_pop();
|
||||
|
||||
/* ---- P1-1 回归:单目录 2100 张图(超过默认 cap=2000)不得「点 A 开 B」 ---- */
|
||||
CHECK(library_test_load(root) == 1, "load root for cap regression");
|
||||
tap_row("Big");
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "tap Big dir stays in library");
|
||||
CHECK(strcmp(library_test_dir(), big) == 0, "entered Big (%s)", library_test_dir());
|
||||
CHECK(library_test_rows() == MR_LIB_SCAN_MAX + 1,
|
||||
"Big rows=%d (cap + truncation row)", library_test_rows());
|
||||
|
||||
/* 截断提示行仍在,且文案不让人误以为「没有更多图」。 */
|
||||
meow_list_item trunc_it;
|
||||
CHECK(library_test_src(library_test_rows() - 1, &trunc_it) == 1 &&
|
||||
trunc_it.enabled == 0 && trunc_it.title && strcmp(trunc_it.title, "已截断") == 0,
|
||||
"truncation row present");
|
||||
CHECK(trunc_it.subtitle && strstr(trunc_it.subtitle, "2000") &&
|
||||
strstr(trunc_it.subtitle, "阅读器"),
|
||||
"truncation text keeps images discoverable: %s",
|
||||
trunc_it.subtitle ? trunc_it.subtitle : "");
|
||||
|
||||
/* 阅读器目录册是**全量** 2100 页(书库 cap 只是列表显示上限)。 */
|
||||
mr_dir_book db;
|
||||
memset(&db, 0, sizeof db);
|
||||
CHECK(mr_dir_book_init(&db, big, 720, cap_fake_load, cap_fake_unload, NULL) == 2100,
|
||||
"reader sees all 2100 pages");
|
||||
|
||||
/* 首 3 + 尾 3 + 20 个确定性伪随机行,逐一点开断言「点哪张开哪张」。 */
|
||||
int picks[26], np = 0;
|
||||
picks[np++] = 0;
|
||||
picks[np++] = 1;
|
||||
picks[np++] = 2;
|
||||
picks[np++] = MR_LIB_SCAN_MAX - 3;
|
||||
picks[np++] = MR_LIB_SCAN_MAX - 2;
|
||||
picks[np++] = MR_LIB_SCAN_MAX - 1;
|
||||
unsigned seed = 12345u;
|
||||
for (int i = 0; i < 20; i++) {
|
||||
seed = seed * 1103515245u + 12345u;
|
||||
picks[np++] = (int)((seed >> 16) % MR_LIB_SCAN_MAX);
|
||||
}
|
||||
|
||||
int mismatched = 0;
|
||||
for (int i = 0; i < np; i++) {
|
||||
int row = picks[i];
|
||||
meow_list_item it;
|
||||
memset(&it, 0, sizeof it);
|
||||
CHECK(library_test_src(row, &it) == 1 && it.title, "pick %d: row source", row);
|
||||
if (!it.title) continue;
|
||||
int want_page = reader_page_of_name(&db, it.title);
|
||||
CHECK(want_page >= 0, "pick %d: %s is a reader page", row, it.title);
|
||||
CHECK(library_test_open_row(row) == 1, "open row %d", row);
|
||||
CHECK(meow_ui_activity_active() == ACT_READER, "pick %d opens reader", row);
|
||||
CHECK(strcmp(mr_reader_dir(), big) == 0, "pick %d reader dir", row);
|
||||
CHECK(mr_reader_ui_count() == 2100, "pick %d reader pages=%d", row,
|
||||
mr_reader_ui_count());
|
||||
if (mr_reader_ui_page() != want_page) mismatched++;
|
||||
CHECK(mr_reader_ui_page() == want_page,
|
||||
"pick %d: clicked %s -> page %d (want %d)", row, it.title,
|
||||
mr_reader_ui_page(), want_page);
|
||||
meow_view_page_pop();
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "pick %d back to library", row);
|
||||
}
|
||||
CHECK(mismatched == 0, "no click/page mismatch across %d picks", np);
|
||||
|
||||
/* 被书库截掉的图(第 2001 张起)在阅读器里仍可打开/翻到。 */
|
||||
CHECK(reader_page_of_name(&db, "p2099.bmp") == 2098, "p2099 lives at page 2098");
|
||||
mr_reader_open_dir_at_name(big, "p2099.bmp");
|
||||
CHECK(meow_ui_activity_active() == ACT_READER, "truncated-out image opens reader");
|
||||
CHECK(mr_reader_ui_count() == 2100, "truncated-out image: all pages present");
|
||||
CHECK(mr_reader_ui_page() == 2098, "truncated-out image opens at its page (%d)",
|
||||
mr_reader_ui_page());
|
||||
meow_view_page_pop();
|
||||
CHECK(meow_ui_activity_active() == ACT_LIBRARY, "back to library after truncated image");
|
||||
CHECK(library_test_rows() == MR_LIB_SCAN_MAX + 1, "library list unchanged after reads");
|
||||
mr_dir_book_free(&db);
|
||||
|
||||
/* 名字找不到(文件已改名/删除):退回续读/首页,不崩、不串册。 */
|
||||
mr_reader_open_dir_at_name(pics, "does-not-exist.bmp");
|
||||
CHECK(meow_ui_activity_active() == ACT_READER, "unknown name still opens reader");
|
||||
CHECK(strcmp(mr_reader_dir(), pics) == 0, "unknown name keeps target dir");
|
||||
CHECK(mr_reader_ui_count() == 6 && mr_reader_ui_open_err() == 0,
|
||||
"unknown name falls back safely (pages=%d err=%d)",
|
||||
mr_reader_ui_count(), mr_reader_ui_open_err());
|
||||
CHECK(mr_reader_ui_page() >= 0 && mr_reader_ui_page() < 6,
|
||||
"unknown name fallback page in range (%d)", mr_reader_ui_page());
|
||||
meow_view_page_pop();
|
||||
|
||||
/* 档案册路径(无 zip 夹具时只验证入口不被当成目录):点 txt 不存在行;跳过。 */
|
||||
|
||||
meow_view_free(reader);
|
||||
meow_view_free(lib);
|
||||
meow_ui_shutdown();
|
||||
rm_rf(data);
|
||||
printf("%s: %s\n", g_fail ? "FAIL" : "PASS",
|
||||
g_fail ? "see failures above" : "library→reader open checks passed");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
/* test_pin.c — 启动 PIN(logic/pin)宿主测试:纯 C,不链 SDK。
|
||||
*
|
||||
* 用内存 KV 存根顶替 meow_config_get/set/int/remove(与 SDK 语义一致:
|
||||
* 未设 → MEOW_CFG_ERR_NOENT;写失败可由 g_fail_write 注入)。
|
||||
* 覆盖:格式校验边界(4/8 位合法;3/9 位、空串、NULL、符号、全角数字非法)、
|
||||
* 设置/校验、错 PIN、重设换盐、change 全分支、disable、配置残缺 fail-closed、
|
||||
* 落盘失败、落盘布局(pin.enabled/hash/salt)。
|
||||
*
|
||||
* 编译(在 tuku/ 下):
|
||||
* gcc -Wall -Wextra -O1 -I. -I"$SDK" test/test_pin.c logic/pin.c -o test/test_pin
|
||||
* 或直接 test/run_tests.sh。
|
||||
*/
|
||||
#include "logic/pin.h"
|
||||
#include "include/meow_ui_config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define KV_MAX 8
|
||||
static struct { char key[64]; char val[128]; int used; } g_kv[KV_MAX];
|
||||
static int g_fail_write; /* 1=模拟全部落盘失败 */
|
||||
static const char *g_fail_key; /* 非 NULL=只让该键写失败(模拟半途失败) */
|
||||
|
||||
static int kv_find(const char *key)
|
||||
{
|
||||
for (int i = 0; i < KV_MAX; i++)
|
||||
if (g_kv[i].used && strcmp(g_kv[i].key, key) == 0) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void kv_clear(void)
|
||||
{
|
||||
memset(g_kv, 0, sizeof g_kv);
|
||||
g_fail_write = 0;
|
||||
g_fail_key = NULL;
|
||||
}
|
||||
|
||||
int meow_config_set(const char *key, const char *val)
|
||||
{
|
||||
if (!key || !key[0]) return MEOW_CFG_ERR_INVAL;
|
||||
if (g_fail_write || (g_fail_key && strcmp(g_fail_key, key) == 0)) return MEOW_CFG_ERR_IO;
|
||||
int i = kv_find(key);
|
||||
if (i < 0) {
|
||||
for (i = 0; i < KV_MAX; i++) if (!g_kv[i].used) break;
|
||||
if (i >= KV_MAX) return MEOW_CFG_ERR_IO;
|
||||
g_kv[i].used = 1;
|
||||
snprintf(g_kv[i].key, sizeof g_kv[i].key, "%s", key);
|
||||
}
|
||||
snprintf(g_kv[i].val, sizeof g_kv[i].val, "%s", val ? val : "");
|
||||
return MEOW_CFG_OK;
|
||||
}
|
||||
|
||||
int meow_config_get(const char *key, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return MEOW_CFG_ERR_INVAL;
|
||||
out[0] = '\0';
|
||||
int i = kv_find(key);
|
||||
if (i < 0) return MEOW_CFG_ERR_NOENT;
|
||||
snprintf(out, (size_t)outsz, "%s", g_kv[i].val);
|
||||
return MEOW_CFG_OK;
|
||||
}
|
||||
|
||||
int meow_config_int(const char *key, int def)
|
||||
{
|
||||
char buf[32];
|
||||
char *end = NULL;
|
||||
long v;
|
||||
if (meow_config_get(key, buf, sizeof buf) != MEOW_CFG_OK) return def;
|
||||
v = strtol(buf, &end, 10);
|
||||
if (end == buf) return def;
|
||||
return (int)v;
|
||||
}
|
||||
|
||||
int meow_config_bool(const char *key, int def)
|
||||
{
|
||||
return meow_config_int(key, def) != 0;
|
||||
}
|
||||
|
||||
void meow_config_remove(const char *key)
|
||||
{
|
||||
int i = kv_find(key);
|
||||
if (i >= 0) g_kv[i].used = 0;
|
||||
}
|
||||
|
||||
const char *meow_config_dir(void) { return "/tmp/meowcfg_test"; }
|
||||
|
||||
static int g_pass, g_fail;
|
||||
#define CHECK(cond, ...) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||
} while (0)
|
||||
|
||||
static int is_hex16(const char *s)
|
||||
{
|
||||
if (!s) return 0;
|
||||
if (strlen(s) != 16) return 0;
|
||||
for (int i = 0; i < 16; i++)
|
||||
if (!((s[i] >= '0' && s[i] <= '9') || (s[i] >= 'a' && s[i] <= 'f'))) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void test_format(void)
|
||||
{
|
||||
CHECK(mr_pin_valid(NULL) == 0, "NULL invalid");
|
||||
CHECK(mr_pin_valid("") == 0, "empty invalid");
|
||||
CHECK(mr_pin_valid("123") == 0, "3 digits invalid");
|
||||
CHECK(mr_pin_valid("1234") == 1, "4 digits valid (min)");
|
||||
CHECK(mr_pin_valid("12345") == 1, "5 digits valid");
|
||||
CHECK(mr_pin_valid("12345678") == 1, "8 digits valid (max)");
|
||||
CHECK(mr_pin_valid("123456789") == 0, "9 digits invalid");
|
||||
CHECK(mr_pin_valid("12a4") == 0, "letter invalid");
|
||||
CHECK(mr_pin_valid("12 4") == 0, "space invalid");
|
||||
CHECK(mr_pin_valid(" 1234") == 0, "leading space invalid");
|
||||
CHECK(mr_pin_valid("1234 ") == 0, "trailing space invalid");
|
||||
CHECK(mr_pin_valid("-123") == 0, "sign invalid");
|
||||
/* 全角数字(UTF-8 EF BC 9x)不是 ASCII 数字 */
|
||||
CHECK(mr_pin_valid("\xEF\xBC\x91\xEF\xBC\x92\xEF\xBC\x93\xEF\xBC\x94") == 0,
|
||||
"fullwidth digits invalid");
|
||||
}
|
||||
|
||||
static void test_default_state(void)
|
||||
{
|
||||
kv_clear();
|
||||
CHECK(mr_pin_enabled() == 0, "default disabled");
|
||||
CHECK(mr_pin_verify("1234") == 0, "verify on disabled = 0");
|
||||
CHECK(mr_pin_set("123") == MR_PIN_ERR_FORMAT, "set short -> format error");
|
||||
CHECK(mr_pin_enabled() == 0, "failed set keeps disabled");
|
||||
}
|
||||
|
||||
static void test_set_verify(void)
|
||||
{
|
||||
kv_clear();
|
||||
CHECK(mr_pin_set("1234") == MR_PIN_OK, "set 1234 ok");
|
||||
CHECK(mr_pin_enabled() == 1, "enabled after set");
|
||||
CHECK(mr_pin_verify("1234") == 1, "verify correct");
|
||||
CHECK(mr_pin_verify("4321") == 0, "verify wrong");
|
||||
CHECK(mr_pin_verify("123") == 0, "verify short");
|
||||
CHECK(mr_pin_verify("12345") == 0, "verify longer");
|
||||
CHECK(mr_pin_verify("") == 0, "verify empty");
|
||||
CHECK(mr_pin_verify(NULL) == 0, "verify NULL");
|
||||
CHECK(mr_pin_verify("12a4") == 0, "verify non-digit");
|
||||
|
||||
char salt[32] = "", hash[32] = "";
|
||||
CHECK(mr_pin_get_salt(salt, sizeof salt) == 1, "salt present");
|
||||
CHECK(mr_pin_get_hash(hash, sizeof hash) == 1, "hash present");
|
||||
CHECK(is_hex16(salt), "salt is 16 hex (got %s)", salt);
|
||||
CHECK(is_hex16(hash), "hash is 16 hex (got %s)", hash);
|
||||
CHECK(strcmp(hash, "1234") != 0, "hash is not the raw pin");
|
||||
|
||||
/* 落盘布局:enabled="1"(明文 PIN 不落盘:配置里只有 16 位 hex 摘要与盐) */
|
||||
char buf[64] = "";
|
||||
CHECK(meow_config_get("pin.enabled", buf, sizeof buf) == MEOW_CFG_OK && strcmp(buf, "1") == 0,
|
||||
"pin.enabled=1 on disk");
|
||||
}
|
||||
|
||||
static void test_reset_new_salt(void)
|
||||
{
|
||||
kv_clear();
|
||||
CHECK(mr_pin_set("1234") == MR_PIN_OK, "set first");
|
||||
char salt1[32] = "", hash1[32] = "";
|
||||
mr_pin_get_salt(salt1, sizeof salt1);
|
||||
mr_pin_get_hash(hash1, sizeof hash1);
|
||||
|
||||
CHECK(mr_pin_set("5678") == MR_PIN_OK, "set second (overwrite)");
|
||||
char salt2[32] = "", hash2[32] = "";
|
||||
mr_pin_get_salt(salt2, sizeof salt2);
|
||||
mr_pin_get_hash(hash2, sizeof hash2);
|
||||
CHECK(strcmp(salt1, salt2) != 0, "new salt on each set");
|
||||
CHECK(strcmp(hash1, hash2) != 0, "new hash on each set");
|
||||
CHECK(mr_pin_verify("5678") == 1, "new pin verifies");
|
||||
CHECK(mr_pin_verify("1234") == 0, "old pin rejected after overwrite");
|
||||
|
||||
/* 同一 PIN 重设:盐不同 → 摘要也不同(防跨设备彩虹表/同值比对)。 */
|
||||
CHECK(mr_pin_set("5678") == MR_PIN_OK, "re-set same pin");
|
||||
char salt3[32] = "", hash3[32] = "";
|
||||
mr_pin_get_salt(salt3, sizeof salt3);
|
||||
mr_pin_get_hash(hash3, sizeof hash3);
|
||||
CHECK(strcmp(salt2, salt3) != 0, "salt differs for same pin");
|
||||
CHECK(strcmp(hash2, hash3) != 0, "hash differs for same pin");
|
||||
CHECK(mr_pin_verify("5678") == 1, "same pin still verifies");
|
||||
}
|
||||
|
||||
static void test_change(void)
|
||||
{
|
||||
kv_clear();
|
||||
CHECK(mr_pin_change("1234", "5678") == MR_PIN_ERR_STATE, "change while disabled -> state");
|
||||
|
||||
CHECK(mr_pin_set("1234") == MR_PIN_OK, "setup 1234");
|
||||
CHECK(mr_pin_change("9999", "5678") == MR_PIN_ERR_WRONG, "change wrong old -> wrong");
|
||||
CHECK(mr_pin_verify("1234") == 1, "old still valid after wrong-change");
|
||||
CHECK(mr_pin_verify("5678") == 0, "new not applied after wrong-change");
|
||||
|
||||
CHECK(mr_pin_change("1234", "12") == MR_PIN_ERR_FORMAT, "change to short new -> format");
|
||||
CHECK(mr_pin_verify("1234") == 1, "old still valid after bad-new");
|
||||
|
||||
CHECK(mr_pin_change("1234", "12345678") == MR_PIN_OK, "change ok (8 digits)");
|
||||
CHECK(mr_pin_verify("12345678") == 1, "new valid after change");
|
||||
CHECK(mr_pin_verify("1234") == 0, "old invalid after change");
|
||||
CHECK(mr_pin_enabled() == 1, "still enabled after change");
|
||||
|
||||
CHECK(mr_pin_change(NULL, "0000") == MR_PIN_ERR_WRONG, "change NULL old -> wrong");
|
||||
CHECK(mr_pin_change("12345678", NULL) == MR_PIN_ERR_FORMAT, "change NULL new -> format");
|
||||
}
|
||||
|
||||
static void test_disable(void)
|
||||
{
|
||||
kv_clear();
|
||||
CHECK(mr_pin_disable() == MR_PIN_OK, "disable while disabled is idempotent");
|
||||
CHECK(mr_pin_set("1234") == MR_PIN_OK, "setup for disable");
|
||||
CHECK(mr_pin_disable() == MR_PIN_OK, "disable ok");
|
||||
CHECK(mr_pin_enabled() == 0, "disabled after disable");
|
||||
CHECK(mr_pin_verify("1234") == 0, "verify fails after disable");
|
||||
|
||||
char buf[32] = "";
|
||||
CHECK(meow_config_get("pin.hash", buf, sizeof buf) == MEOW_CFG_ERR_NOENT, "hash key removed");
|
||||
CHECK(meow_config_get("pin.salt", buf, sizeof buf) == MEOW_CFG_ERR_NOENT, "salt key removed");
|
||||
CHECK(meow_config_get("pin.enabled", buf, sizeof buf) == MEOW_CFG_OK && strcmp(buf, "0") == 0,
|
||||
"enabled=0 on disk");
|
||||
|
||||
/* 禁用后可重新开启(新盐)。 */
|
||||
CHECK(mr_pin_set("8765") == MR_PIN_OK, "re-enable after disable");
|
||||
CHECK(mr_pin_verify("8765") == 1, "re-enabled pin verifies");
|
||||
}
|
||||
|
||||
static void test_corrupt_state(void)
|
||||
{
|
||||
/* enabled=1 但缺 hash → fail-closed */
|
||||
kv_clear();
|
||||
meow_config_set("pin.enabled", "1");
|
||||
meow_config_set("pin.salt", "0011223344556677");
|
||||
CHECK(mr_pin_enabled() == 0, "enabled without hash -> fail closed");
|
||||
CHECK(mr_pin_verify("1234") == 0, "verify fail closed");
|
||||
|
||||
/* hash 长度/字符非法 */
|
||||
kv_clear();
|
||||
meow_config_set("pin.enabled", "1");
|
||||
meow_config_set("pin.salt", "0011223344556677");
|
||||
meow_config_set("pin.hash", "zz");
|
||||
CHECK(mr_pin_enabled() == 0, "garbage hash -> fail closed");
|
||||
|
||||
/* salt 非法 */
|
||||
kv_clear();
|
||||
meow_config_set("pin.enabled", "1");
|
||||
meow_config_set("pin.salt", "");
|
||||
meow_config_set("pin.hash", "0011223344556677");
|
||||
CHECK(mr_pin_enabled() == 0, "empty salt -> fail closed");
|
||||
|
||||
/* change 在残缺状态 → state 错误(不误判为已启用) */
|
||||
CHECK(mr_pin_change("1234", "5678") == MR_PIN_ERR_STATE, "change on corrupt -> state");
|
||||
}
|
||||
|
||||
static void test_store_failure(void)
|
||||
{
|
||||
kv_clear();
|
||||
g_fail_write = 1;
|
||||
CHECK(mr_pin_set("1234") == MR_PIN_ERR_STORE, "set with failing store -> store error");
|
||||
CHECK(mr_pin_enabled() == 0, "still disabled after store failure");
|
||||
g_fail_write = 0;
|
||||
|
||||
/* 写盘失败时不得把状态改成「已启用」——set 先写盐/摘要、最后写 enabled。 */
|
||||
CHECK(mr_pin_set("1234") == MR_PIN_OK, "set ok after store recovers");
|
||||
g_fail_write = 1;
|
||||
CHECK(mr_pin_disable() == MR_PIN_ERR_STORE, "disable with failing store -> store error");
|
||||
g_fail_write = 0;
|
||||
}
|
||||
|
||||
static void test_partial_write(void)
|
||||
{
|
||||
/* 摘要写失败:把盐还原成旧值,已启用的旧 PIN 仍可校验(不会新旧都验不过)。 */
|
||||
kv_clear();
|
||||
CHECK(mr_pin_set("1234") == MR_PIN_OK, "setup 1234");
|
||||
g_fail_key = "pin.hash";
|
||||
CHECK(mr_pin_set("5678") == MR_PIN_ERR_STORE, "hash write failure -> store error");
|
||||
CHECK(mr_pin_enabled() == 1, "still enabled after partial write");
|
||||
CHECK(mr_pin_verify("1234") == 1, "old PIN still valid after partial write");
|
||||
CHECK(mr_pin_verify("5678") == 0, "new PIN not applied after partial write");
|
||||
g_fail_key = NULL;
|
||||
|
||||
/* 重设时 enabled 已是 "1":该键写失败不影响一致性 → 按成功返回。 */
|
||||
g_fail_key = "pin.enabled";
|
||||
CHECK(mr_pin_set("4321") == MR_PIN_OK, "enabled-already-1 write failure is consistent");
|
||||
CHECK(mr_pin_verify("4321") == 1, "new PIN active");
|
||||
g_fail_key = NULL;
|
||||
|
||||
/* 首次开启时 enabled 写失败 → 未启用(fail-closed)。 */
|
||||
kv_clear();
|
||||
g_fail_key = "pin.enabled";
|
||||
CHECK(mr_pin_set("1111") == MR_PIN_ERR_STORE, "first enable with enabled-write failure");
|
||||
CHECK(mr_pin_enabled() == 0, "not enabled after enabled-write failure");
|
||||
g_fail_key = NULL;
|
||||
}
|
||||
|
||||
static void test_verify_does_not_mutate(void)
|
||||
{
|
||||
kv_clear();
|
||||
CHECK(mr_pin_set("2468") == MR_PIN_OK, "setup");
|
||||
char salt1[32] = "", salt2[32] = "";
|
||||
mr_pin_get_salt(salt1, sizeof salt1);
|
||||
(void)mr_pin_verify("0000");
|
||||
(void)mr_pin_verify("2468");
|
||||
mr_pin_get_salt(salt2, sizeof salt2);
|
||||
CHECK(strcmp(salt1, salt2) == 0, "verify does not change salt");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_format();
|
||||
test_default_state();
|
||||
test_set_verify();
|
||||
test_reset_new_salt();
|
||||
test_change();
|
||||
test_disable();
|
||||
test_corrupt_state();
|
||||
test_store_failure();
|
||||
test_partial_write();
|
||||
test_verify_does_not_mutate();
|
||||
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user