Files
LisherSong--ps5-web-file-ma…/HANDOVER.md
T
Songlx516 f85e60ed8c docs: rewrite README around features, move per-version history to CHANGELOG
Both READMEs had grown into an upstream-style append log ("What's new in
v1.7 / v1.8 / v1.8.1 / v1.8.2 / v1.9 / v1.9.2 / v1.9.3M") that also dropped
several features the upstream README still documents. Rewrite both as
feature-oriented descriptions with one shared outline.

- README.md, README.zh-CN.md: feature-oriented rewrite. New sections for
  the archive support matrix, limits and safety, conflict policy,
  password handling, what is deliberately not built, and an
  upstream-vs-us comparison table that includes the rows where upstream
  wins.
- Add header badges (release / license / target / downloads) and two
  for-the-badge buttons (Download, beginner's guide). The release badge
  reads github/v/release so it tracks the latest release instead of
  hard-coding a version; colours are pinned so they do not clash.
- CHANGELOG.md: write the missing [v1.9.1] and [v1.9.2] body sections
  before that prose was removed from the README, so nothing was lost.
- Fix 4 dead links to third_party/unrar/ (renamed unrar7/ in v1.9) and one
  reference to a README section that no longer exists.
- HANDOVER.md: update the handover commit and README structure note.

Documentation only. No binary rebuild, and tag v1.9.3M with its release
asset is untouched.

Verified: link + anchor audit across the tree (now including raw HTML
href/src, not just Markdown syntax) reports 0 problems; table column
counts consistent; bold pairing clean; the new header renders with 6/6
badges loaded at both 1280px and 420px viewports and no horizontal
overflow.
2026-09-25 13:45:40 +08:00

545 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 交接文档 — ps5-web-file-manager 工作进度
> 交接时间:2026-09-25 · 分支 `main` · 最新提交 **`770dcb8`**(「docs: mark v1.9.3M as published」)——tag + Release **`v1.9.3M`** 指向发布提交 `ba668ad`,三者均已推送
>
> **主线(用户 2026-09-12 指令)**:「先从 zip 分卷开始吧,然后把六种组合打齐,并把密码通道补齐,注意一些报错信息提示的时候尽量详细准确」
> **状态:主线全部闭合,格式面已无已知缺口,且已发版。** 六种组合(ZIP/RAR/7z × 单卷/分卷)+ 密码通道(ZIP ZipCrypto/AES + RAR `-p`/`-hp` + 7zAES **含 `-mhe=on` 加密头**)+ 报错详细信息,全部落地、测试全绿、PS5 ELF 构建成功。
> **剩余**:① ~~真机端到端验证~~ **已于 2026-09-24 通过**(用户实机复测三项反馈全过);② ~~发版~~ **已发 `v1.9.3M`**。当前唯一遗留是「含目录条目 + 覆盖模式」第二次解压失败是否属设计意图(见 §六)。
---
## 一、当前状态速览
| 维度 | 状态 |
|---|---|
| 解压引擎 | ZIP / RAR / 7z × 单卷/分卷(6 组合)+ 三种加密(ZIP ZipCrypto/WinZipAES、RAR `-p`/`-hp`、7zAES 与 `-mhe=on` 加密头)全部打通 |
| 主机测试 | **ZIP 140 + RAR 37 = 177 checks,0 失败**(MinGW gcc)+ 7z 套件 **27 用例 0 失败**(`KNOWN_GAPS` 已清空)+ 前端三份:`.build/ui_retry_test.mjs` **40 checks**、`.build/ui_upload_menu_test.mjs` **40 checks**、`.build/preview_check.mjs` **12 断言**(无头 Chromium) |
| PS5 构建 | ✅ WSL prospero-clang 18.1.8,一键脚本可复现;构建已实测**确定性**(同源两次构建 sha256 相同,并用 `cmp` 逐字节验证过) |
| ELF 产物(**已发布**) | `web-file-mgr-v1.9.3M.elf` · 903,448 B · sha256 `8ca47d5aaca75085b32641300cce30fadb7df7749cb6b53d04f129bcecc286b7` · e_machine=0x003e(上传菜单 + 拖拽提示 + 口令重试改按键 id + 错误文案编码修复 + 菜单行高亮层叠修复 + 解压按钮常显置灰;2026-09-24 真机验证通过后发布) |
| 上一个发布产物(回滚点) | `web-file-mgr-v1.9.2.elf` · 870,488 B · sha256 `177e90fecf93a0251e83f67884fba4551051be248330b0d70fda8ea732f88e84`(不含加密改动) |
| GitHub | `main` = `ba668ad`、tag `v1.9.3M`、Release `v1.9.3M` 三者均已推送/发布,资产 sha256 与本地逐字节一致 |
| 已知功能缺口 | **无**(`-mhe=on` 已于 2026-09-23 补齐) |
| README | 2026-09-25 **重写为中英双语「功能导向」结构**(原为上游式的逐版本累加稿):删去六段 `What's new in vX.Y.Z`,改为 *功能 / 压缩包支持矩阵 / 限额与安全 / 快速上手 / 构建 / 使用 / 校验 / 测试 / 项目结构 / **与上游的差异** / 备注 / FAQ / 版本历史*;逐版本正文已迁入 `CHANGELOG.md`(并补上了原先缺正文的 `[v1.9.1]` `[v1.9.2]` 两节)。**注意**:上游 README 里的 `.elf` 一键启动(`localhost:9021`)**本仓源码中已不存在**,重写时未回填 |
### 发布命令(v1.9.2)
```bash
cd "/c/Users/songl/Desktop/Web File Manager/ps5-web-file-manager"
git push origin main
git push origin v1.9.2
gh release create v1.9.2 web-file-mgr-v1.9.2.elf \
--repo LisherSong/ps5-web-file-manager --title "v1.9.2" --notes-file <notes.md>
```
沙箱内 git 出站 HTTPS **可用**(早先记的"被拦"是误判:`timeout 25 git ...`
命中的是 `C:\Windows\System32\TIMEOUT.EXE`,报参数错误而非网络错误)。`gh` 同样可用,
所以 commit / tag / push / 发 Release 都可以在会话里直接跑。
---
## 二、本轮(2026-09-15)变更
### 2.1 一键构建脚本(`a54f34b` / `5229cd5`)
| 文件 | 跑在哪 | 作用 |
|---|---|---|
| `.build/build-win.sh` | Windows Git Bash | 入口:把 WSL 脚本经 **stdin** 喂给 `wsl.exe`,透传退出码 |
| `.build/build-elf-wsl.sh` | WSL Ubuntu-22.04 | 5 阶段:环境检查 → rsync 同步 → `make all` → 验证 → 拷回 Windows |
| `.build/build-elf.sh` | WSL 内 | **仅首次搭环境用**(libmicrohttpd staging 安装 + sudo) |
```bash
# Windows 端(注意:必须 /usr/bin/bash,裸 bash 会解析成 WSL 启动器)
cd "/c/Users/songl/Desktop/Web File Manager/ps5-web-file-manager"
/usr/bin/bash .build/build-win.sh
# 或 WSL 内
bash /home/song/build.sh
```
**两个关键实现点**(改脚本前必读):
- `wsl.exe -- bash -c '...'` 遇到含空格路径会被拆断 → 必须 `wsl.exe -- bash < script.sh`(stdin 重定向)
- `make ... | tail` 会吞掉退出码 → 用 `${PIPESTATUS[0]}`;否则编译失败还会继续跑验证,输出假成功
### 2.2 版本号体系统一(`f4fd464` / `1fa2f09` / `0d036a7`)
原本版本号有**两个真相来源**,已经漂移过:`Makefile` 写 `v1.9.1`,`assets/main.js` 硬编 `"v1.9"`,UI 右下角在整个 v1.9.1 发布期都显示旧值。
现在收敛到 `Makefile` 一处:
```make
VERSION_TAG ?= v1.9.3M # 可用 make VERSION_TAG=v1.9.3 临时覆盖
BIN := web-file-mgr-$(VERSION_TAG).elf
```
改这一行会同时影响 **四处**:ELF 内嵌版本串、PS5 启动通知、输出文件名、UI 右下角。
**尾部 `M` = 改版标记(Modified,LisherSong 维护)**,从 v1.9.3M 起启用。上游
owendswang 的发布版是纯 `vX.Y.Z`,故「带 M = 本仓、不带 = 上游」一眼可分。它刻意
挂在 `VERSION_TAG` 上而不是做一个只管显示的独立常量:这样 `/api/version`、启动通知、
stdout 横幅、UI 右下角、ELF 文件名**五处一次性全覆盖**,不可能只在其中一处漏掉。
附带好处是产物名不再可能与上游同版本号的资产撞车(此前已撞过两次:本地
`web-file-mgr-v1.9.2.elf` 与线上同名资产并存;`-DVERSION_TAG=v1.9.1` 的残留产物
和已发布的 870 488 B 文件尺寸相同)。
| 提交 | 内容 |
|---|---|
| `f4fd464` | `VERSION_TAG` `v1.9` → `v1.9.1`;`.gitignore` 改白名单式(`.build/*` 全忽略 + `!` 放行 6 个脚本),`.workbuddy/` 也忽略 |
| `1fa2f09` | 输出文件名派生自 `VERSION_TAG`;`build-elf-wsl.sh` 从 Makefile 反读版本(不硬编);`build-win.sh` 每次都推 WSL 脚本(原来只在缺失时推,导致改了脚本 WSL 侧仍跑旧版) |
| `0d036a7` | **新增 `/api/version`**,前端右下角改从后端取值(见 2.3) |
⚠️ **`assets/main.js` 里还有第二处字面量** `APP_VERSION_FALLBACK`(`/api/version` 取不到时的兜底值)。
它不在 Makefile 的控制范围内 —— **升版本号时必须一并改**,否则后端请求失败时页脚会显示旧版本。
v1.9.2 就是这两处一起改的。
### 2.3 UI 版本号改由后端提供(`0d036a7`)
**问题**:`assets/main.js:38` 的 `const APP_VERSION = "v1.9"` 与 Makefile 无关,必然漂移。
**修法**:
- 新 `src/version.c` — `GET /api/version` → `{"ok":true,"version":"v1.9.2","titleId":"FMGR88888"}`,直接来自 Makefile 已传的 `-DVERSION_TAG` / `-DTITLE_ID` 宏,没有第二处要记得改
- `src/filemgr.c` 路由表加一行(紧邻 `/api/space`)+ `filemgr_internal.h` 声明 + `Makefile` `COMMON_SRCS`
- 前端:字面量降级为 `APP_VERSION_FALLBACK`(先渲染,保证页脚不空),`loadVersion()` 后台刷新。**请求失败静默吞掉** —— 版本号显示错属于装饰性问题,不该弹错误 toast
**踩坑**:新文件漏了 `#include "json_util.h"` → `strbuf_append` / `json_escape` 隐式声明报错。`space.c` 是模板,照抄时别漏。
---
## 三、功能矩阵与测试
### 3.1 六种组合 + 密码通道
| # | 引擎 | 单卷 | 分卷 | 密码 |
|---|---|---|---|---|
| ① | ZIP | ✅ | ✅ 三种命名约定 | ✅ ZipCrypto + WinZip AES-128/192/256(2026-09-23 打通,此前 `mz_zip.c` 的加密分支没有后端可调) |
| ② | RAR | ✅ | ✅(vendor unrar 7.20.1) | ✅ `RARSetPassword`(`-p` 与 `-hp` 头加密;2026-09-23 接线,此前从未被调用) |
| ③ | 7z | ✅ | ✅ `.7z.001` | ✅ 7zAES(v1.9 起就有)+ `-mhe=on` 加密头(2026-09-23 打通,见 §八) |
### 3.2 测试
```bash
export PATH="/c/mingw64/bin:/c/Users/songl/.workbuddy/binaries/PortableGit/versions/1.2.0/mingw64/bin:/c/Users/songl/.workbuddy/binaries/python/versions/3.13.12:/usr/bin:/bin:/c/Windows/System32:/c/Windows"
cd "/c/Users/songl/Desktop/Web File Manager/ps5-web-file-manager"
/usr/bin/bash tests/run-tests.sh # ZIP 140 + RAR 37 = 177
/usr/bin/bash tests/run-sevenz-tests.sh # 7z 27(KNOWN_GAPS 已清空)
# 前端「密码失败后重试」流程(桩 DOM,无需浏览器)
"/c/Users/songl/.workbuddy/binaries/node/versions/22.22.2-3/node.exe" .build/ui_retry_test.mjs # 27
```
⚠️ **绝不写裸 `bash`** —— 可能解析到 `C:\Windows\System32\bash.exe`(WSL 启动器),脚本跑进 Linux,gcc/python 全变 Linux 版,报莫名错误。必须 `/usr/bin/bash`。
`run-sevenz-tests.sh` 带 `KNOWN_GAPS` 列表(当前仅 `aeshe`),缺口修好后脚本会主动报错,防止列表腐烂。脚本**不做任何删除**(safe-delete 钩子会拦 `rm -rf`)。
---
## 四、构建(PS5 ELF)
### 4.1 日常构建
```bash
cd "/c/Users/songl/Desktop/Web File Manager/ps5-web-file-manager"
/usr/bin/bash .build/build-win.sh
```
增量有效(`ps5-obj/` 缓存保留)→ 二次构建 30s–2min。**别 `make clean`**(全量重编第三方 3–5min)。
产物:项目根 `web-file-mgr-<VERSION_TAG>.elf`,同时留在 WSL `/home/song/ps5-web-file-manager/`。
### 4.2 验证清单
```bash
ls -lh web-file-mgr-v1.9.2.elf
sha256sum web-file-mgr-v1.9.2.elf
od -An -tx2 -j18 -N2 web-file-mgr-v1.9.2.elf # 期望 3e00
strings -a web-file-mgr-v1.9.2.elf | grep -m1 '^v1\.'
```
⚠️ `od -An -tx2` 打印的是**小端 short 的值**(`003e`),不是字节序(`3e00`)。脚本里比对用 `$((16#$EM))` 转数值(**62 = x86-64 ✅ / 183 = aarch64 ❌**)。
✅ **构建已实测可复现**(2026-09-20):同一源码树两次构建 sha256 完全相同;把两处版本字面量回退成 `v1.9.1` 后重构,产物与已发布的 v1.9.1 ELF **逐字节一致**。所以 sha256 可以作为交付指纹用 —— 但它对任何源码改动都会全变(改一个字符串常量会令链接器重排 `.rodata` 字符串池,牵动 `.text` 里所有 RIP 相对位移,原始 diff 会放大到 5 万字节以上,属正常现象,别误判成"代码改了")。核对版本仍推荐 `strings ... | grep '^v1\.'`,最直观。
### 4.3 构建坑(已修,改 Makefile 前必读)
`third_party/7z/AesOpt.c` 用编译器版本宏判断是否启用 AES-NI / AVX / VAES,clang 18 直接进 VAES 分支,但 prospero-clang 默认 target 是 generic x86_64 → `_mm256_aesenc_epi128` 未声明,20 报错。
- ❌ **不能** `filter-out AesOpt.c` —— `Aes.c` 通过 `AesGenTables` 引用 `AesCbc_Encode_HW` 等符号,会链接失败
- ✅ **正解**:路径过滤 `SEVENZ_C_FLAGS := -maes -mavx2 -mvaes`,仅 `third_party/7z/*.c` 用。PS5 是 Zen 2,硬件全支持,运行时无差异
---
## 五、7z 引擎设计要点(改代码前必读)
### 5.1 为什么不走 SDK 的解码器
LZMA SDK 26.03(public domain,已 vendor 到 `third_party/7z/`,解码子集 60 文件)有两个硬限制,**实测复现过**:
1. **`CSzFolder` 上限 4 coder / 3 bond** —— 7-Zip 默认 `-m0=bcj2` 链 = BCJ2 + 4×LZMA2 = 5 coder,`SzAr_DecodeFolder()` 返回 `SZ_ERROR_UNSUPPORTED`。注意 `SzArEx_Open()` 用的是另一套宽松扫描器(`k_Scan_NumCoders_MAX 64`),所以**文件列表和解压尺寸仍然全对**,失败只在解压时按条目暴露
2. **C 解码器完全没有 7zAES coder** —— 所以 SDK 自己既解不了加密内容,也解不了加密头。内容侧由我们的 `sevenz_chain.c` 承担;**头部**侧由 `sevenz_header.c` 承担(见 §八)
→ 因此引擎**自解析 folder blob + 自己驱动 codec 链**(`src/sevenz_chain.c/.h`,pull pipeline:`node_pull(n, dst, want, &got)`,不够就 `node_refill()` 拉上游)。
### 5.2 最关键的坑
**【必记】每个 coder 节点的 `out_size` 必须取 `coder_unpack_sizes[index]`,绝不能用 folder 的 unpack size。** BCJ2 folder 里 MAIN 常大于 folder 最终尺寸(实测 300066 > 300000)。用错的症状:每层 LZMA2 静默短 21 字节,只在特定包上暴露。
其他:
- 编译必须 `-DZ7_PPMD_SUPPORT`,否则 `7zDec.c` 直接丢掉 PPMd
- `CoderUnpackSizes` 是**扁平数组**(每条 = 对应 coder 输出流大小),**非累计**;`FoToCoderUnpackSizes[f]..[f+1]` 是该 folder 的切片
- main coder = 第一个未被 bond 消费的 coder
- `SzArEx_Extract` 失败后会把半成品留在 block cache → 后续条目报**假 CRC**,要重置 `blockIndex`
- 造夹具用 Extra 包的 `7za.exe`(有 PPMd);**`7zr.exe` 没有 PPMd 编码器**
- 调试三件套在 `.build/`:`chainprobe.c`(摸内部图)、`chainprobe2.c -r <coder>`(强制 root 逐层二分)、`chaincheck.py`(Python liblzma 独立复现同一条链,秒判"图错"还是"循环错")
### 5.3 7zAES KDF
`numCyclesPower = b0 & 0x3F`;`saltSize = ((b0>>7)&1) + (b1>>4)`;`ivSize = ((b0>>6)&1) + (b1&0x0F)`,随后依次 salt → iv。
`numCyclesPower == 0x3F` 时 key = `salt||password` 补齐/截断到 32 字节;否则 `key = SHA256(salt || password_utf16le || counter_le64)` 迭代 `1<<numCyclesPower` 次。之后 AES-256-CBC。
限额 `max_aes_cycles = 24`(约 8s)。
**改引擎前先在 `.build/aesprobe.c` 独立验证 KDF**(用 vendor 的 `Sha256.c` + `Aes.c` 解 aes.7z coder0,与 `cus[0]=638314` 比对),确认后再集成。
### 5.4 分卷流抽象
- `src/zipx_volstream.c/.h`(ZIP,包成 `mz_stream`)· `src/sevenz_volstream.c/.h`(7z,包成 SDK `ISeekInStream`)
- **结构体首成员必须是 `mz_stream stream;` / `ISeekInStream vt;`**(回调把 `void*` 强转)
- `vol_is_open()` 必须返回 `MZ_OK`/`MZ_OPEN_ERROR`(**不是 1/0**)
- vtbl **必须注册 `destroy`**,否则 `mz_stream_delete()` 不回调 → 泄漏
- CONCAT 模式对 `DISK_NUMBER`/`DISK_SIZE` 返回 `MZ_PARAM_ERROR` → 让 minizip 不切盘
- DISK 模式 `set_prop(DISK_NUMBER, -1)` 必须切到**最后一卷**(minizip 路径 `mz_zip.c:2252-2275`)
- `remove_source_archives()` 要删**所有**卷,避免孤儿卷
### 5.5 提取门面
`src/sevenz_extract.c`(1753 行)完全仿 `zip_extract.c` / `rar_extract.c`:scan → extract(staging,**不 fsync**) → publish(整 rename) → cleanup。
- **OVERWRITE 与 MERGE 对目录-目录碰撞都递归下钻**(仅叶子文件不同)
- 三方共用 `src/zipx_common.c`(限额 profile + `zipx_status_string()`)
- scan 阶段**每 256 entries** 报一次进度(曾用 4096,小包扫描期 UI 静默),扫描末 force-report;`precheck_folders` 入口也强制报一次
- 密码错时 detail **必须带 archive 名**(曾是 NULL → i18n `{arg}` 展开成空 → 用户看到「密码错误: 」后面光秃秃)
---
## 六、限额体系(ZIP 与 RAR 共用;7z 同源)
| 限额字段 | default | large | 160GB/9万文件场景 |
|---|---|---|---|
| `max_entries` | 200,000 | 500,000 | 9 万 ✅ |
| `max_total_bytes` | 2 TiB | 4 TiB | 160 GiB ✅ |
| `max_file_bytes` | **512 GiB** | **1 TiB** | 20 GiB ✅ |
| `max_ratio` | 500 | 1000 | 仅 ≥1GiB 条目受检 |
| `ratio_min_bytes` | 1 GiB | 1 GiB | 小文件豁免 |
- 切 large 档的触发条件:**压缩包文件本身** >480 GiB(`assets/main.js` `LARGE_FILE_THRESHOLD_BYTES`),160GB 包走 default
- **ratio 有尺寸下限**(`ratio_min_bytes` = 1GiB):小文件高压缩率合法常见(零填充/稀疏),且写出字节受"声明上限 + `check_space()`"双重约束,无害
- **唯一真实失败点是磁盘空间**:`check_space()` 按**解压后总量**查 `statvfs`,峰值 = `zip 体积 + 解出体积`。分卷场景"传一卷解一卷删一卷"可降峰值
- **32 位安全**:引擎内部 size 全 `uint64_t`;minizip `mz_zip.h:34-35` 的 `compressed/uncompressed_size` 是 `int64_t` → >4GiB 不截断
- **已知 UX 缺陷(未修)**:进度条 % 用字节(`main.js:1985`)、文字进度解压时用条目数(`main.js:2014`)、ETA 用字节速度(`task.c:129-177`)。混合大包上割裂,建议统一为字节
---
## 七、环境要点(新人必读)
- **PS5 是 x86-64 Zen 2**(不是 aarch64!),target triple `x86_64-sie-ps5`
- **PS5 SDK C++ runtime = LLVM libc++**(FreeBSD 系 sysroot,无 libstdc++)→ C++ 必须 `-stdlib=libc++`,链接 `-lc++ -lc++abi`(Makefile 已处理:unrar 用 prospero-clang++ 编)
- **PS5 SDK libc 的 `*at()` 族(mkdirat/openat/renameat/unlinkat)能链接但运行时损坏**:返回 -1 且 `errno=0`(2026-09-06 Frostpunk 2 真机确诊)。`zip_extract.c` 已有"*at() 失败回退全路径调用"兼容层;写新引擎代码时直接用全路径或沿用回退模式。报错要带 `(errno=%d)`,`errno=0` 时 `strerror` 会骗人
- WSL:`export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk` 后才能 make
- `target/user/homebrew/` 由 songl(197609) 拥有,WSL 身份 song(1000) 写不进 → staging 模式(make install 到 /tmp → `sudo cp -r`)
- `//wsl$/Ubuntu-22.04/` 是 SMB 只读视图,改 WSL 文件必须走 Windows 路径
- libmicrohttpd 必须 `--disable-https --disable-openssl`
- **minizip-ng 4.2.2 补丁(升级会丢)**:`src/mz_strm_os_posix.c` L25 后插 `#ifndef O_BINARY / #define O_BINARY 0 / #endif`
- **主机 POSIX shim(CP936 主机必需)**:`tests/posix_compat.h` 把 `lstat/stat → wfm_stat`、`opendir → _wopendir`(UTF-8 转换)、`fopen → _wfopen`。MinGW ANSI 入口看不见 UTF-8 文件名
- 复杂 commit / tag message 用 `-F 文件`,不要 `-m` 长文本(bash quoting 会挂)
---
## 八、7z `-mhe=on` 加密头(2026-09-23 已闭合)
**它为什么难**:`-mhe=on` 时整个 header 也是一条独立的 7z 流——归档末尾的下一头部区域以 `k7zIdEncodedHeader`(0x17)开头,后接一段 StreamsInfo,描述「一个 folder,其输出就是真正的 header」。而 vendored SDK 的 **C 解码器没有 7zAES coder**,`SzArEx_Open2()` 走到 `SzAr_DecodeFolder()` 就返回 `SZ_ERROR_UNSUPPORTED`,于是**连文件列表都读不出来**(文件名、folder 表、每个条目的尺寸全在那份加密头里)。
**做法**(`src/sevenz_header.{c,h}`):
1. 自己读 32 字节 start header,只探**一个字节**——不是 0x17 就立刻 `SZH_PLAIN` 收工(普通的 `-mhc=on` 压缩头、`-mhc=off` 明文头都走这条,SDK 行为一字不变)。
2. 是 0x17 就整段读下来(CRC 校验),**最小解析** PackInfo + UnpackInfo:pack 位置/大小、folder 的 coder 描述字节范围、每个 coder 的 unpack size、folder CRC。解析刻意宽容——任何异常一律回落 `SZH_PLAIN`,把诊断权留给 SDK,保证非加密归档的报错一字不改。
3. 把这份描述喂给 **`sz_chain_parse()` / `sz_chain_decode()`**,也就是内容走的同一条 7zAES 路径,密码规则完全一致:需要密码而没给 → `SZH_ERR_PASSWORD`;解出来 CRC 不对 / 不是 `k7zIdHeader` → 同样是密码错。
4. 造一个**虚拟 `ISeekInStream`**:`[0,32)` 是改写过的 start header(指向明文头),`[hdr_off, hdr_off+L)` 是解出来的明文头,其余一律透传真实归档。`hdr_off` 就用加密头原本所在的偏移,所以**归档里存的任何一个偏移都不用搬**——SDK 在它预期的位置读到明文头,从明文头推出的 dataPos 依旧指向真实的内容 pack 流。
5. 交给 `SzArEx_Open()`,之后一切照旧(内容仍由 `sevenz_chain.c` 直接读 `sevenz_volstream`)。
**要点/坑**:
- 明文头比它替换掉的那条记录**长**(实测 aeshe.7z:记录 64 B、明文 462 B),所以虚拟流的 `total` 要取 `max(真实文件长度, hdr_off+L)`,否则 `SzArEx_Open2()` 的 seek-to-END 长度检查会报 `SZ_ERROR_INPUT_EOF`。
- **`LookToRead2_INIT` 不 seek**,第一次 `Look` 从真实流当前位置读。`szh_prepare()` 会把流移来移去,所以交给 SDK 之前必须显式 seek 回 0(`sevenz_extract.c` 里那一段有注释)。这原本是个隐性依赖。
- 明文头大小有上限(`SZH_MAX_HEADER` 64 MiB),sink 按需增长、不信头部里声明的 unpack size。
- 只处理 `numFolders == 1`(SDK 自己对这条记录就传 `numFoldersMax = 1`)与 `external == 0`。
**覆盖**:`tests/fixtures-7z/aeshe.7z`(密码 `Secret123`),`tests/run-sevenz-tests.sh` 的 `KNOWN_GAPS` 已清空——chain 驱动与 façade 两条路径都跑通;`test_sevenz_extract.c --cases` 另验无密码 / 错密码 → `ZIPX_ERR_PASSWORD`、正确密码 → 成功,且失败后不留 staging。
### 真机端到端待验证
ELF 已构建,但需装 PS5 实测:
1. ZIP / RAR / 7z 三类**分卷**真机解压
2. **加密 7z(7zAES + `-mhe=on` 加密头)** 真机解压(`aeshe.7z` 那类归档在真机上连文件列表都要走新代码)
3. 160GB / 9.5 万文件大 ZIP
4. 分卷 RAR 进度条实时走动
5. UI 右下角版本号显示(`/api/version` 与兜底字面量应一致)
> **📊 2026-09-23 首个真机性能数据**:解一个 **18 GB 的包**,**11 分钟**、**1252 个条目**
> (平均 14.7 MB),UI 报 **10–40 MB/s**;**18 GB 是压缩包自身的大小**(✅ 已确认)。
> **格式 = RAR**(✅ 已确认);包原本在 **PC 上**,**经插件上传**进 PS5,**上传速度 30–40 MB/s**。
> 口径是**解压后的字节**(`zip_extract.c:651-678` 累加 `uncompressed_size`,`:900/910` 累加
> `write()` 写出的解压字节),且是 **250 ms 采样的瞬时值**(`task.c:202-206`,进度只在 ≥1 MiB
> 时上报)—— 摆动里含采样噪声,**只有「总字节 ÷ 总耗时」可信**。
> 仍然成立的一条:**per-entry 开销不是主因**(平均 14.7 MB/条目,不是小文件场景)。
>
> **⚠️ 2026-09-23 晚 更正:本节原先写的「解码不是瓶颈」已撤回。** 两条理由:
> ① 它拿「PS5 解 **RAR**」的 28 MiB/s 去比「PC 解 **7z**」的 427 MiB/s —— **不同格式、不同
> 解码器、不同机器**,量级论证不成立;② 「4× 摆动 = 解码无罪」此前已降级为 250 ms 采样
> 噪声的弱证据。**原先那句「源盘交付速度 ≈ 28 MiB/s 是硬上界」同样站不住** —— 它是从总耗时
> 反推的*观测结果*,不是设备能力上限;若解码是瓶颈,源盘恰恰没跑满。
>
> **③ 新发现(有据可查、且直接针对真实负载):RAR 解码在 PS5 上是单线程的。**
> `third_party/unrar7/os.hpp:43-45` 的 `#define RAR_SMP` 落在 `#ifdef _WIN_ALL` 分支**内**
> ⇒ POSIX 构建不定义(我们 Makefile 里 0 次出现),而官方 POSIX makefile 第 11 行是
> `DEFINES=… -DRAR_SMP` —— **我们漏了这个开关**。后果:`unpack50mt.cpp`
> (`Unpack::Unpack5MT`,rarlab 专门调过的多线程 RAR5 解压器)**没编进来**,
> `unpack.cpp:185-198` 的 MT 分支整段不参与编译,`SetThreads` / `ThreadPool` 一并消失。
> ⇒ **首要假设:28 MiB/s ≈ 单线程 RAR5 解码的正常量级**(`18 GB ÷ 660 s` 是**解码输入**速率;
> 而上传实测证明**写入端**至少能到 30–40 MB/s、**读取通常快于写入** ⇒ 纯存储上限解释不了它)。
> ⚠️ 自查一条:**用 ELF 符号表查内部符号是无效手段** —— 该 ELF 只有 `.dynsym`(513 项)、
> **无 `.symtab`**,「零命中」是假象(本次差点据此误判)。结论来自 Makefile 与 `os.hpp`。
> **下一步**:先在 PC/WSL 上 A/B(`-DRAR_SMP` + `unpack50mt.cpp` + `-pthread`,跑同一批 RAR5
> fixture 并保证 37 项 RAR 断言全绿),收益显著再上真机;同时真机补两个小事实
> (**RAR4 还是 RAR5**、**解压后多大**)与 `T_copy`。详见 `docs/EXTRACTION-PERF.md` §六 文首
> 更正块与 `docs/REAL-CONSOLE-PROFILE.md`。
>
> **⇒ 终局(2026-09-23 18:15,用户决定):这条线不做。** 不做的依据是**当前证据判不了收益**,
> 而不是没收益:MT 只并行**解码**(worker 只跑 `unpack50mt.cpp:190` 的 `UnpackDecodeThread`),
> 写盘恒为主线程串行(`UnpWriteBuf()` 只在 `unpack50mt.cpp:283/475/587` 被主线程调用)——
> 若瓶颈在写路径(真机上传已证明写入端只有 30–40 MB/s),收益退化为 1.0×。要判定必须先做
> `T_copy`(拿插件自己的 `TASK_COPY` 搬同一份包,`src/filemgr.c:836`),再决定是否值得改构建
> 并刷机验证;用户选择停在第一步之前。**重开的第一个动作是 `T_copy`,不是改 `-DRAR_SMP`。**
### 可选项(非阻塞)
- **性能**:**优化前**实测上游(7-Zip 本体)在 7z 格式上快 1.9×(单线程)/ 3.4×(8 线程);ZIP 无显著差异。差距不在我们的架构(我们比 SDK 自己的 `SzArEx` 路径还快 1.02×)。**已全部落地(2026-09-16)**:①汇编解码器(`LzmaDecOpt.asm`+jwasm,1.26×,无 jwasm 自动退纯 C)②多线程 LZMA2(`Lzma2DecMt`,8 线程,1.37×,线程失败自动降级 chain;BCJ2/加密布局仍走 chain)③ZIP 逐条目 fsync 移除(8000 文件 ≥14×)。7z 现与 7-Zip 单线程打平、ZIP 已压过官方(本机受 Defender 拖累不可比,PS5 无该因素)。RAR 与官方 UnRAR 同速(unrar 自带 `target("aes")` SIMD 已启用,无逐条目 fsync)。完整数据见 `docs/EXTRACTION-PERF.md`,基准工具 `tests/bench_driver.py`。**⚠️ 但「单线程打平 / 8 线程 1.59×」是在最有利的输入形状上测的**:基准归档是 `-m0=lzma2 -ms=on` 的**单文件**(`tests/bench_driver.py:179`),恰好是唯一能让 `sz_chain_lzma2_root()`(`src/sevenz_chain.c:750`,要求 1 coder / 0 bond / 1 pack stream / 纯 LZMA2)生效的形状;真实的**多 folder / BCJ2 / 7zAES** 归档会让 MT 失效、退回单线程 chain —— 所以那个 1.59× **既不是上限也不是下限,方向未知**。剩余优化(CRC 硬件化、MT 扩到 BCJ2、条目级并行、ZIP inflate 换 libdeflate、AES-NI)**全部集中在 7z 多线程这一条线上**,建议**先拿真实归档在真机上 profile 再排序**,别按 PC 上这份数字动手
- ~~fsync 批量化(每 64MB/N 条刷一次)~~ → **已作废,改为「彻底移除」**(2026-09-16):实际落地的不是批量刷,而是把 ZIP 引擎的逐条目 fsync 直接删掉(RAR/7z 本来就没有),三引擎统一为「**不 sync、只 rename**」——publish 是纯 rename、也没有续解功能,该 fsync 无收益。8000 文件 fixture:fsync 版 >200 s 未跑完 → 无 fsync **14.5 s(≥14×)**。已知取舍:publish 之后到落盘之间断电,可能出现「文件在但内容不完整」;要补只需在 extract 收尾做**一次**目录/整盘 flush(PS5 是 FreeBSD 系,`syncfs()` 不一定有,`sync()` 是全盘、偏重)。代码现状见 `src/zip_extract.c:937-945`,实测见 `docs/EXTRACTION-PERF.md:18-21`
- 解压失败保留 staging 支持续解(中等改动)
- ~~进度条 % / 文字进度 / ETA 三处口径统一为字节~~ → **已完成**(`assets/main.js:2071-2079`,条目计数已移除并注明原因)
---
## 九、仓库许可与代码归属(2026-09-15 核查)
用户曾担心「项目源自他人代码、没有许可」——**前提不成立**:
- 上游 `owendswang/ps5-web-file-manager` 经 GitHub API 确认 = **GPL-3.0**(78 stars,last push 2026-09-08)
- 本项目 `LICENSE`(GPL-3.0 全文)在 root commit `5cb0b76` 即存在,与上游一致
- 授权链完整:`ps5-payload-dev/websrv`(John Törnblom, GPLv3+,其 Copyright 头仍保留在 `asset.c` / `asset.h` / `mime.h` / `websrv.h`)→ `owendswang` → 本项目
代码量构成:
- 第三方 vendored **71,528 行**(unrar7 27,710 / zlib 20,106 / LZMA SDK 17,248 / minizip-ng 6,464)——重写时原样复用,零成本
- 第一方 20,844 行 = 上游 v1.7 遗产 13,860 + 自有 6,984
- **自有代码中 3,661 行零耦合**(`sevenz_chain` 2094 + `zipx_volume` 658 + `zipx_volstream` 494 + `sevenz_volstream` 415,只依赖 public domain / zlib)→ 可单独抽成 MIT 库
完整评估见 `docs/REWRITE-FEASIBILITY.md`(三路径:补合规 0.5 天 / 架构重构 12–18 天 / clean-room 重写 35–50 天)。**结论:建议补合规而非重写** —— GPL-3.0 保护 7z 引擎成果不被闭源白嫖。
---
## 十、工作区状态
✅ **2026-09-24:工作树已重新干净并发布** —— 该批次改动(59 个文件)已提交为 `ba668ad`、打 tag `v1.9.3M` 并发布 Release,资产与本地逐字节一致。以下为当时(2026-09-23 起)的状态记录,保留作历史。
以下为 2026-09-15 的历史清理记录(当时工作树干净,"仅剩有意保留的未跟踪文档")。
已清理(2026-09-15):
| 文件 | 说明 | 去向 |
|---|---|---|
| `erssonglDesktopWeb File Managerps5-web-file-manager¬`(2543 B) | 早期 shell 转义事故:一次 `git log --oneline --color` 的输出被重定向进了文件名。末尾是 U+F022(私用区码位,mojibake 残留),各工具渲染不一 —— git 显示成八进制转义、`ls -b` 印成 ASCII 引号 | **回收站**(`$R…`,2543 B,可还原) |
| `web-file-mgr-unpack 1.9.1.elf`(898 KiB) | 陷阱:文件名写 1.9.1,内嵌却是 9-07 的 **v1.9**(无 7z 引擎) | 已不在仓库根 |
> ⚠️ **清理这类特殊文件名时**:`SHFileOperationW`(带 `FOF_ALLOWUNDO` 走回收站)对含私用区码位的路径会返回 `ERROR_FILE_NOT_FOUND (2)`,**但动作实际已生效**。删完务必查 `C:\$Recycle.Bin\<SID>\$I*` 记录确认落在回收站(`$I` 存原路径 UTF-16,`$R` 是内容)。本沙箱里 `Add-Type` 与 `rm` 都被拦(后者有 safe-delete 钩子),只能用 Python `ctypes` 调 shell32。
`.build/` 下的探针/调试产物已被 `.gitignore` 白名单覆盖,不再污染 `git status`。
---
## 十一、本轮(2026-09-23)变更:加密通道补齐(已随 v1.9.3M 发布)
**目标**:让 ZIP 与 RAR 的加密归档真正可解(7zAES 早已可用)。两者此前都报
`extract_unsupported`,但**缺口在引擎侧,不在 UI** —— 密码框、`password=` 字段、
`err_extract_password` 文案从 v1.9 起就已就位。
### 11.1 ZIP:给裁剪过的 minizip-ng 补一个 crypto 后端
`third_party/minizip-ng` 是裁到只读路径的精简副本,`mz_zip.c` 里
`#ifdef HAVE_WZAES / HAVE_PKCRYPT` 的分支保留着,但**对应的流与 crypto 后端被裁掉了**。
本轮补回:
| 文件 | 状态 | 说明 |
|---|---|---|
| `src/mz_strm_wzaes.{c,h}` | 上游 4.2.2 原样恢复 | WinZip AES 流(方法 99 + `0x9901` 扩展字段) |
| `src/mz_strm_pkcrypt.{c,h}` | 上游 4.2.2 原样恢复 | 传统 PKWARE / ZipCrypto 流 |
| `src/mz_crypt_wfm.c` | **新写**(~860 行) | 本地 crypto 后端:SHA-1、HMAC-SHA1、AES-128/192/256 |
后端要点:
- S-box 与 GF(2^8) log/alog 表**首次使用时推导**,所以不新增 `.rodata` 查表(实测 `.rodata` 仅 +256 B,是字符串)。
- 随机数直接 `open("/dev/urandom")`,**不要**走 `mz_os_rand()` —— 后者会退回 `rand()`/`srand()`,把两个新符号塞进导入表。最终产物**动态符号零新增**。
- PBKDF2 复用 vendored 的 `mz_crypt.c`(与上游逐字节一致),没有重写。
- 非 SHA-1 算法与 AEAD aad 一律返回 `MZ_SUPPORT_ERROR`(本项目只读,不需要)。
- KAT 先行:写完后先用 FIPS 197 / RFC 3174 / RFC 2202 / RFC 6070 / SP 800-38A
向量单独验算(`.build/kat_crypto.c`,24/24),再接线。踩到的三个坑:AES 仿射用 `rol32`
应为 `rol8`;GF 乘法 `(uint8_t)(a+b) % 255` 截断,应全程 int;HMAC 的 ipad 必须由
**已 XOR 过 0x5c 的 opad** 再推。
### 11.2 RAR:把 `RARSetPassword` 接上
`src/rar_extract.{c,h}`:新增 `password` 形参(`rar_extract()` 为第 8 个参数)。
调用点是 `RAROpenArchiveEx` 之后、**首次 `RARReadHeaderEx` 之前**——这是解密 `-hp`
头加密归档的硬性顺序要求(scan 与 extract 两个阶段各自开档,两处都要设)。
`ERAR_MISSING_PASSWORD` / `ERAR_BAD_PASSWORD` 由 `ZIPX_ERR_UNSUPPORTED` 改映射为
`ZIPX_ERR_PASSWORD`;`RHDF_ENCRYPTED` 只在**没给密码**时提前拒绝。
### 11.3 前端:密码失败后自动重试(这步不做,功能等于不可达)
原先密码框**只对 7z 弹**(`actionExtract()` 里的 `isSevenZipArchive()` 判断),
ZIP/RAR 加密归档失败后用户根本没机会输密码。现在 `handleTerminalTask()` 在
`op === "extract" && error_code === "extract_password"` 时走
`retryExtractWithPassword()`:
- 记忆原始请求(`extractRetryKey(task.id)` → `{conflict, removeSource, name, large, attempts}`),
重试时**保持冲突策略与大文件选配**;
- **必须按任务 id 记,不能按路径记**(v1.9.3M 后期修正)。路径在传输中被
「服务端 JSON 逐字节转义为 `\u00XX`」+「`fs_path_value()` 反向还原成原始字节」这一对
转换改了表示 ⇒ **非 ASCII 目录下**「页面手里的路径」≠「任务回报的路径」,按路径查必然
落空 ⇒ 口令框永远不弹,用户只看到一个失败框,必须先手动再解压一次。任务 id 由服务端
分配、原样回传,不受编码影响;重试时也改用**服务端回报的** `task.src` / `task.dst` 重发。
消费即删(重试注册到新 id 下),Map 最多留 8 条(同一时刻只可能有一个活动任务)。
- 最多 3 次;取消或空输入即放弃,回落到原有失败提示;
- 7z 保留提前询问(免得白跑一次 scan + folder 解析)。
新增文案 `extractPasswordRetryAsk`(重试:密码不正确)+ `extractPasswordFirstAsk`(首次:
此压缩包已加密),与提前询问用的 `extractPasswordAsk` 区分 —— 第一次失败时用户还没输过密码,
再说「密码不正确」就是误导。
错误文案里的条目名必须过 `decodeFsText()`:`backendErrorText()` 原样用了 `error_arg`,
而服务端把它逐字节转义过 ⇒ 中文/日文条目名在错误框里显示成 `â®…ç§.psd`。列表侧一直有这层
翻译(`displayName()`),只有错误文案漏了。
同源的编码坑:`pathJoin(服务端回报的目录, 本地文件名)` 把两种表示混进同一个字符串,而
`fs_path_value()` **只要发现任一个码点 > 0xFF 就整体不修** ⇒ 中文名文件放进中文名目录时
路径失效。新增 `encodeFsText()`(`decodeFsText()` 的逆)在拼接前把本地名转成同一表示,
`uploadAndExtractFile()` 与 `actionNewText()` 两处都用它。
无头回归:`.build/ui_retry_test.mjs`(真 `main.js` 载入桩 DOM,40 checks,含「非 ASCII 目录
必须仍弹口令框」的回归用例)、`.build/ui_upload_menu_test.mjs`(40 checks:i18n 键覆盖、
菜单接线、样式、高亮规则的层叠作用域,以及**解压按钮不许被隐藏、只许被置灰**)、
`.build/preview_check.mjs`(无头 Chromium 跑真页面,验菜单开关、页脚布局、**解压按钮的
显隐/置灰/提示随选区变化**,并**读回三种交互状态下高亮的计算值**;该脚本已改为失败即
非零退出)。
**菜单行的「选中高亮」曾被两条规则同时破坏**(用户报「选中下面那个高亮效果不对」):
① 全局 `button:focus` 的 `outline: 3px + offset 2px` 是按 54px 工具栏按钮设计的,套在 46px
菜单行上会越过面板 6px 内边距、压住相邻行,且 outline 的圆角半径不随 offset 自适应 ⇒ 视觉上
成了一个「脱离的框 + 两侧挂着的弧线」;② 面板自己的
`.upload-menu-list button:hover:not(:disabled)` **从未生效过** —— 它与
`button:not(.row-action):hover:not(:disabled)` 特异性同为 `(0,3,1)`,而后者在文件里更靠后 ⇒
后者胜出,于是 hover 是 `#303945`、focus 是 `#2b343e`,**两个高亮两个颜色**,且一行 hover 时
另一行仍因 focus 亮着 ⇒ 看起来「两行同时被选中」。修法:两条规则都收敛到面板 id
(`#uploadMenu button:…`,`(1,1,1)` / `(1,2,1)` 稳赢通用规则),行只用填充表示选中,键盘焦点
提示改为**行内 `inset` 环**(`box-shadow: inset 0 0 0 2px`)—— 画在行内,任何行高都不可能
越界。**这类坑只有真引擎读计算值才抓得住**,光看源码两条规则都「像是对的」。
**解压按钮改为「常显 + 置灰」**(用户要求「直接显示出来 只不过是灰色的 只有能解压的文件才可以
点击」):`index.html` 去掉 `hidden`,`renderExtractButton()` 不再碰 `.hidden`,改成按选区设
disabled 并给一条说明原因的工具提示(什么都没选 ⇒ 新增 `extractSelectArchive`;只选中子卷 ⇒
沿用 `extractSelectMainVolume`;选中**多个** ⇒ 新增 `extractOneAtATime`,旧代码这种情况错用了
「请改选主卷」,文案本身是错的)。🪤 **`button:disabled` 带 `pointer-events: none` ⇒ 禁用按钮
无法 hover,`title` 永远不弹** —— 必须像既有的 `.parent-nav-button:disabled` 那样把
`pointer-events` 还回来(点击仍无效,`disabled` 属性本身挡激活)。标签同时从 `extractToCurrent`
(「解压到当前目录」)换成短词 `extract`(「解压」),与工具栏其他动词一致:**常显按钮不该
同时又是最宽的那个**(英文下 `Extract to current folder` 会到 107 px)。
**代价必须实测而不是估**:按钮宽 96 px ⇒ 工具栏换行阈值(zh)1080 → 1190 px、(en)1230 →
1350 px。`.build/preview_check.mjs` 已把阈值**钉成断言**(1920/1600/1280 必须都是一行),
并按四种选区验 disabled / opacity / title;该脚本同时从「只打印」改成**失败即非零退出**。
### 11.4 构建坑:编译选项变化必须让目标文件失效(**改 Makefile 前必读**)
`make` **看不见**编译选项变化。加 `-DHAVE_WZAES -DHAVE_PKCRYPT` 后,
`mz_zip.o` / `mz_crypt.o` 被判定为最新而复用 → 此时已无线程引用新流 →
`--gc-sections` 把加密代码再丢一次,**链接却报成功**(本次第一次构建的产物与
已发布 v1.9.2 **逐字节相同**,`readelf` 才发现 `.text` 只长了 336 B)。
修法(取代原先的 `LzmaDec.o` 特例):把第三方编译选项写进标记文件,
内容变了才重编。
```make
PS5_FLAGS_STAMP := ps5-obj/.third_party_cflags
LINUX_FLAGS_STAMP := linux-obj/.third_party_cflags
$(PS5_FLAGS_STAMP): FORCE
@printf '%s\n' '$(THIRD_PARTY_C_FLAGS_7Z) $(LZMA_DEC_OPT_FLAG)' > $@.tmp
@cmp -s $@.tmp $@ || { mv -f $@.tmp $@; echo ' [cflags] ...'; }
```
> 诊断手法:拿未 strip 的产物比 `readelf -S` 各段尺寸,而不是看总体积。
> 改一个字符串常量会重排 `.rodata` 字符串池,字节 diff 会被放大到几万字节,
> 但段尺寸是守恒的——判断"代码到底有没有变"要看段尺寸 + 助记符序列。
> 现成脚本:`.build/_seccmp.py`、`.build/_operandcheck.sh`。
### 11.5 产物与验证
| 项 | 值 |
|---|---|
| 主机测试 | ZIP 140 + RAR 37 = **177 checks / 0 失败**(`tests/run-tests.sh`) |
| 前端测试 | **27 checks / 0 失败**(`.build/ui_retry_test.mjs`) |
| ELF | 870,680 B · sha256 `b1409f5c1bc4b1a39ab337853b956f4807f95c5770dee6eca7a18a62cc08f80e` · e_machine 0x003e(加密轮结束时;`-mhe=on` 之后的产物见 §12.3) |
| 确定性 | 同一源码树构建两次逐字节一致 |
| 段变化(vs 已发布 v1.9.2) | `.text` +11,296 · `.bss` +5,120(AES 表) · `.rodata` +256 · 动态符号零新增 |
| 内嵌资产核验 | ELF 内 gzip 资源中可检出 `retryExtractWithPassword` / `extractPasswordRetryAsk`(普通 `strings` 找不到,要先解 gzip;脚本 `.build/check-elf-gzip.py`) |
**未做(发版前必做)**:未 commit / tag / 发 Release;真机端到端未验。
版本号**已升**为 `v1.9.3M`(2026-09-24 加改版标记 `M`,见 §2.2)。
README(中英)、CHANGELOG、本文档已同步为「已随 v1.9.3M 发布」状态。
---
## 十二、本轮(2026-09-23)变更:7z `-mhe=on` 加密头(已随 v1.9.3M 发布)
**目标**:补上最后一个 7z 格式缺口(设计与坑见 §八)。
### 12.1 新增
| 文件 | 说明 |
|---|---|
| `src/sevenz_header.{c,h}` | **新写**(~900 行)。头部读取 + `k7zIdEncodedHeader` 最小解析 + 虚拟 `ISeekInStream` |
| `Makefile` | `src/sevenz_header.c` 进 `COMMON_SRCS`(PS5 与 linux 共用) |
| `tests/run-sevenz-tests.sh` | 编 `sevenz_header.o` 进 `ENGINE_OBJS`;`KNOWN_GAPS` 清空;façade 矩阵加入 `aeshe` |
| `tests/sevenz_chain_e2e.c` | 按与产品相同的顺序接线 `szh_prepare()`(否则 chain 矩阵读不了 `aeshe`) |
| `tests/test_sevenz_extract.c` | `aeshe` 三例(无密码 / 错密码 → `ZIPX_ERR_PASSWORD`;正确密码 → 成功),另修一处 `snprintf` 截断告警 |
### 12.2 关键设计(细节见 §八)
- 只探**一个字节**:不是 `0x17` 立刻返回 `SZH_PLAIN`,SDK 行为与改动前完全一致(12 个既有 fixture 全部复跑通过)。
- 解析刻意宽容:PackInfo/UnpackInfo 之外的任何异常都回落 `SZH_PLAIN`,把诊断权留给 SDK。
- 复用 `sz_chain_parse()` / `sz_chain_decode()`,所以 7zAES 的密码/错误语义与内容侧**完全同源**,不新增第二个 crypto 实现。
- 虚拟流的 `total` 必须 `max(真实长度, hdr_off + L)`;`LookToRead2_INIT` 不 seek,交回 SDK 前必须显式 seek 到 0。
### 12.3 产物与验证
| 项 | 值 |
|---|---|
| 7z 套件 | **27 用例 / 0 失败**,`aeshe` 在 chain 与 façade 两条路径都 `ok`,`KNOWN_GAPS` 为空 |
| 主机测试(ZIP/RAR 回归) | ZIP 140 + RAR 37 = **177 checks / 0 失败**(无回归) |
| ELF | 903,448 B · sha256 `8ca47d5aaca75085b32641300cce30fadb7df7749cb6b53d04f129bcecc286b7` · e_machine 0x003e(== 本轮最终产物,见 §12.4) |
| 确定性 | 同一源码树构建两次 sha256 相同 |
| 段变化(解压按钮常显 vs 上一轮) | **只有 `.rodata` 变化**:`0x026CC0` → `0x026F00`(+0x240 = 576 B:index.html 去掉 `hidden` 并换短标签、`main.js` 的三条禁用理由、两份语言文件各两条新文案、`.extract-action:disabled` 及其注释)。`.text` 两次均为 `0x087780`、`.data` 均为 `0x00034C` —— 第六次「只改内嵌前端资源」 |
| 段变化(菜单行高亮修复 vs 上一轮) | **只有 `.rodata` 变化**:`0x026B40` → `0x026CC0`(+0x180 = 384 B,三条收敛后的高亮规则加其注释)。`.text` 两次 readelf 均为 `0x087780` —— 又一次「只改内嵌前端资源、不碰 C 逻辑」的标准形状 |
| 段变化(本轮前端三项 vs 上一轮) | **只有 `.rodata` 变化**:`0x026A40` → `0x026B40`(+0x100 = 256 B)。`.text` / `.data` / `.eh_frame*` 一字节未变 —— 「只改内嵌前端资源 + 加两条文案」的标准形状 |
| 段变化(加 `M` 标记 + 修正 `err_extract_unsupported` 文案 vs 加密轮产物) | **只有 `.rodata` 变化**:加 `M` 标记 +0x100(256 B),修正文案再 +0x40(64 B);`.text` / `.data` / `.bss` / `.eh_frame*` / `.gcc_except_table` **一个字节都没变**。又因 16 KiB 段对齐留有余量,**六次构建的文件总尺寸都是 903,448 B**:尺寸相同**不代表**二进制相同(sha256 逐个不同:`f3164efa…` → `53296d29…` → `7b5ab00c…` → `212107a6…` → `da36834d…` → `cf2c0fcf…` → `8ca47d5a…`) |
| 段变化(加密轮 vs 其前一轮) | `.text` +4,880 · `.rodata` +640 · `.eh_frame_hdr` +32 · `.eh_frame` +160 —— 正文合计 **+5,712**;其余 **+27,056** 是 `p_align=0x4000` 的两处段对齐填充(LOAD#1 越过 0x8C000 边界)。**段数仍为 20,动态符号零新增(513 → 513)** |
> 判读提示:这次文件涨了 32,768 B,但正文只涨 5,712 B —— 不要按体积下结论。
> 权威做法是比较**段尺寸**与**动态符号集合**(见 §11.4 的诊断手法)。
**未做(发版前必做)**:未 commit / tag / 发 Release;真机端到端未验。
版本号已升为 `v1.9.3M`(改版标记 `M` 于 2026-09-24 加入)。