Files
LisherSong--ps5-web-file-ma…/docs/UPSTREAM-V1.8-COMPARISON.md
T
Songlx516 ba668ade50 release: v1.9.3M -- encrypted archives, plus the UI round that followed
First release under the fork-marker convention: VERSION_TAG carries a trailing
`M`, so /api/version, the PS5 start-up notification, the stdout banner, the UI
footer and the ELF file name all read "v1.9.3M" in one move -- and a fork build
can no longer collide with an upstream artifact of the same version, a mix-up
that already happened twice. The footer carries a tooltip spelling the marker
out.

Two bodies of work.

1. Encrypted archives (ZIP / RAR / 7z)

   - ZIP: ZipCrypto (traditional PKWARE) and WinZip AES-256, through
     minizip-ng plus a vendored crypto layer (mz_crypt_wfm.c,
     mz_strm_pkcrypt.c, mz_strm_wzaes.c).
   - RAR: RARSetPassword, wired after RAROpenArchiveEx and before the first
     RARReadHeaderEx. The ordering is load-bearing, not stylistic.
   - 7z: 7zAES including -mhe=on encrypted headers, via a virtual
     ISeekInStream that splices a pseudo-header + the real archive + the
     decrypted header, so no offset stored inside the archive has to move.

   A wrong password is reported as ZIPX_ERR_PASSWORD, and a failed attempt
   leaves no staging directory behind.

2. Reporting, and the UI round that on-device testing produced

   - A RAR whose dictionary exceeds what the build supports now gets its own
     extract_dict_too_large code instead of being mis-reported as "entry too
     large"; the message names both the required and the supported size. The
     behaviour is deliberately unchanged -- such archives are still refused,
     because admitting one means allocating the whole window up front, which
     is why rarlab's own CLI refuses them by default.
   - The upload entry is a menu again: one "Upload" button opening "Upload
     files / Upload folder". The previous main-button-plus-small-arrow made
     "upload folder" effectively undiscoverable.
   - A drag-and-drop hint sits in the footer (hidden on the console browser,
     where drag is not how anyone uploads).
   - The extract button is now always present and merely disabled until
     exactly one archive is selected, instead of appearing out of nowhere.
   - Upload-and-extract on an encrypted archive now prompts for the password
     directly. The retry table used to be keyed by PATH, and for a non-ASCII
     directory the string the page holds and the string the server reports
     are not the same bytes -- the lookup missed, so the user got a bare
     error box and had to press Extract by hand before the prompt appeared.
     It is now keyed by task id, which the server assigns and echoes back
     verbatim.
   - Error text passes through decodeFsText(), so a GBK entry name no longer
     surfaces as `â®…ç§.psd`.
   - Local names are encoded with encodeFsText() before being joined onto a
     server-side path. fs_path_value() declines to rewrite a path if ANY code
     point exceeds 0xFF, so concatenating a local name onto a server directory
     produced a mixed representation and a silently dead path.
   - The menu row highlight was losing the cascade to the generic button rule
     (identical specificity, later in the file) while inheriting the toolbar's
     3px focus ring, which overflowed a 46px row. Both rules are now scoped to
     the panel and the keyboard cue is an inset ring, so it cannot escape the
     row at any line height.
   - The footer status line is clamped to a single line; a long
     "uploading 3/12: some-name.zip" used to wrap out of the 46px footer.
   - A first failed password attempt now says the archive is encrypted,
     instead of blaming a password the user was never asked for.

Artifact
  web-file-mgr-v1.9.3M.elf
  903,448 B
  sha256 8ca47d5aaca75085b32641300cce30fadb7df7749cb6b53d04f129bcecc286b7
  e_machine 0x003e (x86-64 / PS5)

  The file size is identical to the four builds before it, and every one of
  them carries a different sha256: only .rodata moved, and by less than the
  16 KiB section alignment absorbs. Compare sections with `readelf -SW` --
  never infer "nothing changed" from the byte count.

Verification
  - host suites: 140 ZIP + 37 RAR = 177 checks, 0 failures
  - 7z suite: 27 cases, 0 failures. The `aeshe` entry that used to sit in
    KNOWN_GAPS is gone -- the -mhe=on fixture now passes both the folder
    decoder and the extraction facade
  - frontend: .build/ui_retry_test.mjs (40 checks), .build/ui_upload_menu_test.mjs
    (40 checks), .build/preview_check.mjs (12 assertions in headless Chromium
    against the real page and a fixture API). Two layout regressions and the
    highlight cascade bug were caught by the last one and by nothing else --
    reading the source, both CSS rules "look correct"
  - built twice from this tree: byte-identical (cmp clean). rsync refreshes
    every asset mtime, so this is a genuine recompile, not make short-circuiting
    on unchanged sources
  - embedded assets verified in place with .build/check-elf-gzip.py, because
    gen-asset-module.py gzips them and plain `strings` finds none of their text
  - exercised end to end on a real PS5; the checklist is
    docs/DEVICE-TEST-v1.9.3M.md

Docs
  - docs/USER-GUIDE-zh-CN.md (new, simplified Chinese user guide)
  - docs/DEVICE-TEST-v1.9.3M.md (new, on-device acceptance checklist)
  - docs/REAL-CONSOLE-PROFILE.md (new, measured console behaviour)
  - docs/archive/HANDOVER-v1.8-planning.md (superseded v1.8 design notes)
  - CHANGELOG / README (both languages) / HANDOVER updated with the artifact
    fingerprint, the section deltas and the new test counts
2026-09-24 21:07:12 +08:00

11 KiB
Raw Blame History

上游 v1.8 解压方案 vs 本项目 v1.9.1

核查时间:2026-09-15 · 上游 owendswang/ps5-web-file-manager 来源:GitHub API 查询 + commit b405721("Added support for 7zip helper",2026-09-08)完整 patch(2301 行) 上游 v1.8 = tag ad7d754,v1.7 = 72341d6(本项目 fork 的基线)

结论速览

不是同一个层面的方案,各有明确胜负手。

上游 v1.8 本项目 v1.9.1
一句话 把 7-Zip 本体做成外部 helper 进程,靠 IPC 调用 自研 + vendor 解码库,全部内嵌在同一进程
最强的点 格式覆盖 30 种,解压核心是 7-Zip 本体 完整安全护栏 + 单文件部署
最弱的点 零安全护栏,且 helper 缺失 = 功能全废 格式覆盖只有 3 种

一、上游 v1.8 的实际架构

1.1 三个组件

组件 位置 职责
src/archive_extract.c(124 行) 本仓库 只做后缀识别 + 输出目录名推导
src/archive_helper.c(732 行) 本仓库 IPC 客户端:启动 helper + Unix socket 协议
wfm-7zip-helper.elf(~百 MB 级) /data/wfm/,不在仓库里,单独分发 真正的解压 = 7-Zip 本体

README 原文:

Extraction requires the separately distributed wfm-7zip-helper.elf helper at /data/wfm/wfm-7zip-helper.elf.

1.2 启动链路

/* archive_helper_autostart() —— 仅 __SCE__(PS5)分支,Linux 直接返回 0 */
1. archive_helper_probe()     // 已有实例在跑就复用,绝不替换
2. stat("/data/wfm/wfm-7zip-helper.elf")   // 不存在 → 静默返回 0
3. 校验 ELF magic "\x7fELF"、大小 4B ~ 128MB
4. connect(127.0.0.1:9021)    // WFM_ELFLDR_PORT —— elfldr payload 加载器
5. 把 helper ELF 的**全部字节流**推过去
6. shutdown(SHUT_WR)

即:通过 elfldr(PS5 homebrew 的 ELF 加载 payload)把 helper 拉起成一个独立进程。

1.3 通信协议(自研二进制帧)

  • 传输层:Unix domain socket —— PS5 走 /system_tmp/wfm-7zip-helper.sock,Linux 走 /tmp/...
  • 帧格式:magic "W7HP" + 20 字节头(type / flags / request_id / payload_size,大端序)
  • 上限:payload 1 MiB、路径 256 KiB、响应 64 KiB

消息类型:

方向 消息
主 → helper PING EXTRACT CANCEL LIST_TASKS ATTACH_TASK ACK_TASK
helper → 主 PONG ACCEPTED PROGRESS CURRENT_FILE PASSWORD_REQUIRED DONE ERROR TASK_SNAPSHOT LIST_DONE

回调接口 archive_helper_callbacks_t:cancel_requested() / progress(done,total) / current_file(path)。

1.4 支持格式(30 种后缀)

.7z .001 .zip .zipx .rar .arj .bz2 .bzip2 .tbz .tbz2 .cab .gz .gzip
.tgz .tpz .lzh .lha .tar .xz .txz .z .taz .zst .tzst .xar .xip
.cpio .lzma .pmd

外加 .partNN.rar(只接受 part1,即必须从第一卷进入)。 分卷靠 7-Zip 原生能力:.001 无差别接受(不校验卷集连续性)。

1.5 任务恢复(上游的亮点)

filemgr_recover_extract_tasks() 在 main.c 启动时调用:从 helper 拉 TASK_SNAPSHOT 列表,把还在跑的 job reattach 回主进程的任务列表。

因为 helper 是独立进程,主 payload 被重启 / 浏览器重开,解压任务不会丢。archive_helper_probe() 的注释也点明了这个设计的意图:

/* Never replace a connected daemon, even if it is temporarily slow. */

1.6 ⚠️ 没有的东西(全 patch 逐行核查)

项目 上游 v1.8 说明
条目数上限 ❌ 无 max_entries 类逻辑
单文件/总大小上限 ❌ 无
压缩比筛查(防炸弹) ❌ 无
磁盘空间预检 ❌ 无 statvfs 调用
路径穿越防护 ❌ 未见 ../绝对路径校验,交给 7-Zip
原子发布 ❌(未见) 直接解到目标目录,中断留半成品

grep -i "ratio|max_entries|statvfs|bomb" 的全部命中都是误报(operations 里含子串 ratio)。

换句话说:上游把解压这件事整体外包给了 7-Zip,包括安全责任。

二、本项目 v1.9.1 的架构

组件 职责
src/zip_extract.c ZIP:minizip-ng,含 zip64、三种分卷命名、.z01 真分盘语义
src/rar_extract.c RAR:vendor unrar 7.20.1(DLL 模式),v4/v5/多卷/加密
src/sevenz_extract.c + sevenz_chain.c 7z:自解析 folder + pull 式 codec 链 + 7zAES
src/zipx_volume.c / zipx_volstream.c / sevenz_volstream.c 卷集识别 + 连续流抽象

格式覆盖:.zip / .rar / .7z 三种,各自支持单卷 / 分卷 / 密码。

已有的工程能力

项目 本项目 实现位置
条目数 / 总大小 / 单文件上限 ✅ 两档 profile(20万50万条目 / 24 TiB / 512 GiB~1 TiB) zipx_common.c
压缩比筛查 ✅ max_ratio 500/1000,1 GiB 下限豁免小文件 zip_extract.c
磁盘空间预检 ✅ check_space() 按解压后总量查 statvfs zip_extract.c:636
路径穿越防护 ✅ 有专项测试(path traversal variants) 测试矩阵
原子发布 ✅ staging 目录 + 整 rename(无逐条目 fsync,2026-09-16 起) 三引擎统一
冲突策略 ✅ FAIL / OVERWRITE / MERGE,目录碰撞递归下钻 三引擎统一
取消 ✅ 条目粒度 —
任务恢复 ❌ 没有 —
内存隔离 ❌ 与主进程共享地址空间(LZMA2 字典须封顶) —

测试覆盖

ZIP 108 + RAR 27 + 7z 28 = 163 checks,0 失败(MinGW host)+ PS5 真机构建通过。

三、逐维度对比

维度 上游 v1.8 本项目 v1.9.1 胜
格式覆盖 30 种 3 种 上游
解压核心正确性 7-Zip 本体(20 年验证) 自研 7z 链 + 成熟 vendor 库 上游
分卷语义 靠 7-Zip 原生(.001 无差别) 自研两套语义(byte-split / zip split disk) 平手(我们更细,上游更省心)
.rar.001 ✅ 直接吃 ⚠️ 需改名为 .partN.rar 上游
部署 两个文件,路径写死 /data/wfm/ 单文件,零外部依赖 我们
helper 缺失时 功能全废(archive_helper_not_running) 不适用 我们
防压缩炸弹 ❌ 无 ✅ ratio + 1 GiB 下限 我们
磁盘写满保护 ❌ 无 ✅ 预检解压后总量 我们
路径穿越 ❌ 无 ✅ 有防护 + 测试 我们
中断留残留 ⚠️ 可能留半成品 ✅ staging 隔离,失败即清 我们
任务恢复 / 跨重启 ✅ 跨进程 reattach ❌ 上游
内存隔离 ✅ 独立进程,峰值不影响主服务 ❌ 共享地址空间 上游
主仓库构建成本 低(不编 7-Zip) 首次 +3~5 min、ELF +98 KiB 上游
错误信息详细度 中等(6 个 code) 含条目名 / errno / 字节数 我们

四、该怎么评价

上游那步棋走对了什么

把 7-Zip 当外部依赖,是性价比极高的工程决策。 自己写解码器要几个月,apt 一个 7-Zip 就换来 30 种格式 + 20 年验证的正确性。而且顺手拿到了两个我们暂时没有的能力:跨进程任务恢复、内存隔离。

但它把安全责任也一起外包了

这是实质缺陷,不是风格问题。在 PS5 上跑的具体后果:

  1. 压缩炸弹直接写满内置存储 —— 一个 10 KB 的 zip 可以声明 100 GB,没有任何拦截
  2. 路径穿越 —— ../../ 条目可以写到解压目标之外(7-Zip 本身会做基本清理,但这属于"相信第三方"而非"自己保证")
  3. 磁盘写满 —— 不预检,写到 ENOSPC 才失败,此时已留下部分文件
  4. 失败留残留 —— 没有 staging 隔离

我们在这四项上都有明确实现和测试。163 checks 里专门有一组 path traversal variants 和 limits。

但必须承认格式覆盖是短板

31 种格式的差距不是"多一点便利",是用户会觉得我们弱:.tar.gz、.xz、.zst、.bz2 在 PS5 场景(游戏包、备份、Mod)里出现频率不低。

五、可借鉴 / 不建议照抄

建议做:补常见格式(性价比高)

按实际收益排序:

优先级 格式 实现路径
高 .tar / .tar.gz / .tgz tar 解析器自己写(格式极简,~300 行)+ zlib 已在手
高 .gz / .xz / .lzma gzip 用 zlib;xz/lzma 可 vendor liblzma 或复用 LZMA SDK 的 LzmaDec
中 .bz2 / .zst 单文件解码器,各 ~1000 行,可 vendor
低 .cab / .arj / .lzh / .cpio / .xar 罕见,除非有具体需求

注意:gzip/xz/zst 是单文件格式(不是归档),解出来就是一个文件,输出路径语义需单独设计。

建议评估:任务恢复 / 进程隔离

动机:主 payload 被系统杀或用户重开浏览器时,正在跑的大包解压会整个丢失。上游靠 helper 独立进程解决了这点。

但在我们架构下的成本:需要引入子进程 + IPC(或至少状态持久化 + 重启后重扫 staging)。PS5 上 fork/exec 与 elfldr 强耦合,不是小改动。

中间路线:解压失败时保留 staging 目录 + 记录任务清单文件,重启后支持"续解"。改动量中等,能拿到大部分收益,不必引入 IPC。

不建议:照抄 helper 路线

理由:

  1. 部署体验倒退 —— 用户要装两个文件,还得记住放 /data/wfm/;丢一个功能全废。现在单 ELF 是无状态交付,这是真实优势
  2. 安全护栏会一起丢 —— 走 7-Zip 就意味着放弃我们对 entries/ratio/空间/穿越的控制
  3. helper 上游自己都不敢放进仓库("separately distributed"),大概率是体积或许可原因,跟着走会继承同样的问题
  4. 我们已经付过的成本会沉没 —— 7z 引擎(自解析 folder + pull 链 + 7zAES)+ 三类分卷抽象共约 3,600 行零耦合代码

一句话总结

上游赢在"格式广度 + 进程架构",我们赢在"安全 + 部署 + 错误质量"。

如果只想要功能广度,上游的路线更省力;如果要一个能放心交付给用户、不会被一个恶意压缩包搞崩存储的工具,我们的路线是对的,缺的只是格式覆盖 —— 而那是可以在现有架构里增量补的,不需要推倒重来。

附:核查方法备忘

# 拿某个 commit 的完整 patch(不要用 WebFetch,会被 AI 摘要截断)
curl -sSL --ssl-no-revoke -o up.patch \
  "https://github.com/<owner>/<repo>/commit/<sha>.patch"

# 沙箱内 curl 必须加 --ssl-no-revoke,否则 schannel 报
# CRYPT_E_NO_REVOCATION_CHECK (0x80092012)

# 提取单个文件的 diff
sed -n '/^diff --git a\/src\/foo.c/,/^diff --git a\/src\/bar/p' up.patch

# 只看新增行(去掉 diff 前缀)
... | grep '^+' | sed 's/^+//'