88 Commits
Author SHA1 Message Date
Songlx516 ce55d08299 feat: 解压目标目录可选 + PKG 安装 ABI/权限修复
- 前端:解压前弹出目标目录对话框,支持手改路径与「浏览目录」导航
  选择器逐级选目录;冲突策略、7z 密码、大文件模式一并纳入。后端
  /api/extract 原本就收 dst_dir,无需改动。上传即解压仍落当前路径。
- pkg_installer.c:元数据 ABI 由 0x38 改为原生 0x30(移除 Mono 托管层
  多算的 slot/is_playgo_enabled 两个字段,旧布局会让 InstallByPackage
  参数错位);安装前补 kernel_set_ucred_authid(0x4800000000000006),
  权限失败以 PKG_INSTALL_PRIV_FAILED 区分;Makefile 链接 -lkernel_sys。
- i18n / HTML / CSS 同步新增解压对话框与目录选择器文案与样式。

注:PS5 专有改动(ABI/authid/链接)未经真机验证,需在 kstuff/etaHEN
环境实测安装路径是否真跑通。
2026-09-29 16:27:27 +08:00
Songlx516 c46f5969c8 build(check): make the demo5 sweep assertion able to fail
The old pin ("running progress bars sweep, capacity bars do not") only read
animationName — but getComputedStyle is readable on a display:none subtree, and
the element it found was the .track.pulse inside #view-tasks, hidden by default.
So it was green while nothing on screen was animating. That is exactly the
defect the user reported ("demo5 has no flashing progress bar").

ui_demos_check.mjs
- strength: require a non-zero layout box AND animationName !== "none"; switch to
  #view-tasks inside the assertion and switch back (synchronous, so it stays
  atomic under Promise.all). The negative half only checks the track's
  visibility, not the fill's — a queued row's fill is legitimately 0 wide.
- scope: also cover the task table's progress column (.row .bar[data-p]), which
  is the same task as the big card; classifying .bar by class had filed it under
  "capacity meter, never animate".
- print the total at the end: the "N assertions" figure in the docs had been
  obtained by grepping the output and was one too high (claimed 99, actually 98).
- comment: 8888 -> 2026 (the default port changed, the comment did not).

proposal_check.mjs
- keyword 99 项 -> 98 项, matching the measured count.
2026-09-27 13:03:57 +08:00
Songlx516 9c06fca17f test(ui): 候选收敛到 1/5 + 定名/端口/状态口径/动效成族的断言
第十一轮用户五条:去掉 etaHEN 状态显示、新项目起新名字与新端口、HTTP 与 SMB
服务也用「小绿灯」、demo 只留 1 和 5、把 demo1 进度条的闪光效果移植到 demo5
并在其他能加的地方也加。

拍板:项目定名 PS5 Nexus(仓库/ELF ps5-nexus),默认端口 2026。

- ui_demos_check.mjs:DEMOS 5 档 → 2 档(B/C/D 归档到工作区根的
  _retired-demos/),删掉对应断言 209 → 96;再加 3 条回归钉(demo1 三盏绿灯
  与地址都在吸顶导航条里且状态区不列打包发行版、demo5 状态区口径、demo5
  运行中进度条有扫光而静态容量条没有)→ 99 项全过。
- proposal_check.mjs:209 → 99,并新增 8 条命名/状态/动效关键词断言,另加
  一条「8888 只能作为『已改掉』的引述出现」的引述约束。

注:断言一律用渲染后的纯文本片段。textContent 会剥离标签,写
"<code>2026</code>" 这类关键词永远匹配不上,且不会抛错(假失败)。
2026-09-27 12:46:19 +08:00
Songlx516 400784942e test(ui): 本机地址断言 + 1100 断点档 + 地址硬规则
- ui_demos_check.mjs:
  · 新增本机地址断言(形如 IP:PORT / 非回环 / 命中区 ≥40px /
    点一下有可见反馈且 1.5s 后复原 / PS5 档在首屏内)
  · 命中区必须量 ::after —— 视觉高度按风格只有 24~37px,
    只量 getBoundingClientRect() 会把「看着小但点得中」误判成不合格
  · 回归钉改为逐个 await(点了按钮要等一个 tick 才看得到结果)
  · 对比度采样 25 → 30(每个 demo 加一处地址文字),
    备用主题下额外复测一次地址(--fg2/--fg3 是主题令牌,换色就可能掉出阈值)
  · 断点矩阵补 1100 档:1280 与 390 都给绿时,1100 上风格 C 顶栏溢出 24px
- proposal_check.mjs: 新增 8 条本机地址硬规则关键词断言
2026-09-27 11:07:58 +08:00
Songlx516 cfa02562b2 test(ui): 加 PS5 视口档与逐 demo 回归钉
背景:用户在 PS5 上直接打开这个插件用,屏幕形状与桌面不同
(浏览器可视区约 1920×970,横向充裕、纵向紧缺),且本轮起
游戏页要显示 PKG 封面、存档页对标 Garlic SaveMgr 的三栏骨架。

- ui_demos_check.mjs
  · 新增 PS5 视口档(1920×970):断言一级导航单行不折行、落在
    首屏顶部、无横向溢出,并逐个切视图再各量一遍
  · 单页长滚动的 demo(B/C/D)没有一级导航 ⇒ 导航项 SKIP 而不是
    FAIL —— 假失败和假通过一样会让人忽略整组断言
  · 新增 EXTRA 逐 demo 回归钉:demo1(导航置顶后触控目标仍 ≥44px、
    不再是左侧竖栏)、demo5(封面网格 ≥6、含抽不到 icon0.png 的
    回退态与加密锁定态、筛选真会筛、存档页有快照列与操作日志终端、
    空状态可来回切换)
  · EXTRA 的判定必须以字符串形式传入:playwright 的 evaluate 在
    序列化阶段就拒绝函数数组
- proposal_check.mjs
  · 新增 8 条界面硬规则关键词断言(kstuff 启动 / 1920×970 /
    顶部单行吸顶 / sce_sys/icon0.png / .covers/ / 抽不到必须回退 /
    底部操作日志终端 / 以及刻意不抄的那四个动作)
2026-09-27 10:02:49 +08:00
Songlx516 9a03fb7c05 test(ui): measure every view, not just the one that happens to be visible
Style A and E are view-switched pages (mutually exclusive `.view` containers).
The runner only ever measured the default view, so the other four were
unguarded — and the switch itself was never asserted. Style E's first version
had navigation that only moved the `aria-current` highlight while the content
stayed identical, which is exactly the failure this now catches.

Both gaps paid for themselves immediately:

- style A: `view-tasks` overflowed 65px at 390px. `.task` is a grid item whose
  `min-width:auto` resolved to min-content (371px) and burst the single column
  out of a 288px content area. Fixed with an explicit `minmax(0,1fr)` track,
  plus `overflow-wrap:anywhere` on the mono path line (a long
  `Media/Streaming/…/pak` segment has no break opportunity).
- style E: the fake navigation had to be replaced by real view switching.

Also tightened two things that were measuring the wrong element:

- style E's contrast targets moved into the default view — reading
  `getComputedStyle` on a `display:none` node returns values, but "measuring an
  element nobody can see" is not a check.
- the focus-ring group now switches to a view that actually contains a
  focusable row; otherwise the "inline inset ring" convention was silently
  skipped and the nav button was measured instead.

140 assertions, all passing.
2026-09-27 09:46:19 +08:00
Songlx516 9740a252a9 docs(check): pin the save-writeback "forced snapshot" vacuum as a regression
Enumerated every independent save-writeback implementation by fingerprinting the one
system call a write-back cannot avoid — sceFsCreatePfsSaveDataImage. 25 code hits,
minus SDK stubs and verbatim derived copies leaves 5 distinct projects; read each
write path. Result: 0 of 5 force a snapshot before write-back.

  garlic-savemgr      copy_file(local, ORIGINAL) with O_TRUNC -> the original is
                      truncated in place; no copy at all when it already lives on
                      /data/.  (src/main.c:419 + :692)
  garlic-worker       no write-back step at all, and save_periodic_cleanup()
                      unlinks every leftover copy -> using it as a backup loses
                      the save.  (src/ps5/savedata.c:287-298)
  elf-arsenal family  verbatim copy of the above
  ps5-sd-tool         tmp image -> copy_recursive -> unmount; 0 backup keywords
  apollo-ps4          real backup UI, but _addBackupCommands() lists "Apply Changes
                      & Resign" BEFORE the "File Backup" section -> edit first
  savescum (MIT)      best backup UX (timestamped), but restore never forces a
                      backup first, and FTP sees only plaintext
  vsh-utils/trophies  0 hits

So this is the domain's only vacuum, and it is a live data-loss path in the shipped
competitor rather than merely a missing feature. Recorded as the domain's admission
criterion, not an optional extra.

proposal_check.mjs:
- tables 14 -> 15 (new section 2-2-bis evidence table)
- NEW named assertion: no table may exceed the 960px content column. The generic
  page-overflow check did catch this, but pointed at the page instead of the cause.
  TRAP: the sheet's global `td:first-child{white-space:nowrap}` also matches a
  `td[colspan]` full of prose (it IS the first child of its row), so one note row
  rendered as a single 2265px line and blew the table out to 2085px. Fix was to
  remove the conflict — move the prose out of the table — not to raise specificity.
- keyword assertions for the four hard rules and the evidence
- quotedOnly guard: the mis-stated install-layer AuthID 0x3800000000000010 (zero hits
  in etaHEN; section 6 carried a stale copy of it until now) may only survive inside
  a correction, never as a live claim
2026-09-27 09:28:12 +08:00
Songlx516 9e0032c019 test(ui-demos): 将风格 E 纳入无头 UI 验收
- DEMOS 注册 ps5-ui-demo-5-harness.html,并补 5 个对比度采样点
- 备用主题分支由「仅 demo4 暗色」改为按 demo 配置:
  D 亮→暗、E 暗→亮,两个方向都验(原来只测了一半)
- 截图前先 scrollTo(0,0):焦点组调用 el.focus() 会把目标行滚进视口,
  导致长页 demo 的整页截图拍到中段而非首屏
2026-09-26 19:57:54 +08:00
Songlx516 3af704f854 test(ui-demos): pin archive-suffix stripping for the new-subdir name
The four UI demos and the proposal spelled the auto-created folder as
`PPSA28495-app.rar/` -- the archive's full filename including its
extension. The folder must be `PPSA28495-app/`.

- demos 1-4: the subdir target is now `.../PPSA28495-app/`
- proposal: add the derivation spec -- strip the WHOLE archive suffix,
  not just the last extension (`a.part01.rar` -> `a`, not `a.part01`;
  `a.7z.001` -> `a`, not `a.7z`), reusing the regexes already in
  assets/main.js:816-869 -- plus a worked strip table covering .rar /
  .zip / .7z / RAR5 volumes / 7z byte-split sets
- proposal_check: table count 13 -> 14, assert the spec text survives,
  and quotedOnly("-app.rar/") so the wrong target cannot return as a
  live claim
- ui_demos_check: assert no `.(zip|rar|7z)/` path appears in any demo
2026-09-26 17:58:53 +08:00
Songlx516 2fb1a1cf56 test(ui): add headless UI-demo checker; whitelist it
Four UI style demos for the rewrite project now share one information
architecture, so the review criteria that keep them honest are worth
pinning as assertions rather than eyeballing:

- horizontal overflow at 1920 / 1280 / 390, including *after* opening
  the notes drawer and the extract dialog — a transform-translated
  drawer still extends the scrollable area, and `visibility:hidden`
  does not stop it
- WCAG-AA contrast on 20 sampled text/background pairs, measured from
  computed styles with an ancestor walk for the effective background.
  This is how a 4.43:1 secondary colour was found (threshold 4.5:1)
- focus ring actually renders, and the focused row stays inside its
  parent box (the inline-inset-ring convention for 46-52px list rows)
- no `button[disabled][title]` — `disabled` implies
  `pointer-events:none`, so the tooltip explaining *why* a button is
  disabled can never appear; the demos use `aria-disabled` instead
- content baseline: the four nav domains, the kstuff platform chip, the
  average-speed progress wording, the save-mount singleton notice, the
  pick-a-directory extract target, and "make subdirectory" defaulting
  to unchecked

The script also screenshots each demo and inlines them into the
workspace-root comparison page as data URIs (kept single-file, no
external requests), marking placeholders with a `data-shot` attribute
so re-runs are idempotent.

104 assertions pass.
2026-09-26 17:39:52 +08:00
Songlx516 9f3e91241e test(proposal-check): pin the singleDPI / no-etaHEN install-layer facts
Round 7 of the rewrite proposal added section 2-1-bis, which pins the install
path to a self-built installer that needs kstuff only (no etaHEN). Guard the
load-bearing facts so a later edit cannot silently drop them:

- GPL-3.0-or-later reuse right + the NOTICE attribution obligation
- the AuthID that actually works: DEBUG_AUTHID 0x4800000000000006
  (the documented 0x3800000000000010 has zero hits in the etaHEN source)
- self-elevation (kernel_set_ucred_authid / -lkernel_sys) and status polling
  (sceAppInstUtilGetInstallStatus), both of which this repo currently lacks
- the MetaInfo 0x38 -> 0x30 ABI correction: slot / is_playgo_enabled came from
  the Mono managed signature, not from the native struct
- DPI v2 being browser-reachable (Access-Control-Allow-Origin)

Also generalise the falsified-claim guard into quotedOnly() and add a second
case for "the SDK grants the permission" (rebutted by singleDPI's explicit
kernel_set_ucred_authid call, which is what actually runs on the console).

Tables 10 -> 13; 37 assertions, all passing.
2026-09-26 16:53:21 +08:00
Songlx516 de4b74c354 test(proposal-check): fix blind 390px table assertion, add install-layer guards
- 旧的窄屏表格断言用 scrollWidth/clientWidth 比较,而 `table{overflow:hidden}`
  让二者恒等 ⇒ 该断言永远不可能触发(假绿)。改为量 bounding rect 与视口
  比较,并放行 .dectable 横向滚动容器内的宽表 —— 正是这个盲点让 §8 表格
  的 12px 溢出漏检。
- 新增 6 条内容断言:kstuff / VoidShell / elf-arsenal / /proc/kstuff /
  autoload.txt / "没安装过 etaHEN"。
- 新增 1 条反向断言:被证伪的「HEN 事实标准就是 etaHEN」不得以断言形式
  回归;作为历史引文(上下文含 作废/修正/已删除)则允许存在。
- 结构断言:表格 9 → 10(新增 §2③ 竞品格局)。
2026-09-26 16:31:49 +08:00
Songlx516 fb7ac30c15 test: track the rewrite-proposal render check
`.build/proposal_check.mjs` verifies `ps5-nas-rewrite-proposal.html` -- a
document that deliberately lives outside the repo -- by loading it in real
Chromium and asserting: zero external requests, no horizontal overflow at
1100px and 390px, and the structural counts its sections depend on.

It was the last harness with no copy anywhere but this machine, so clearing
`.build` would have lost it. Adds it to the .gitignore allowlist next to the
other render checks and extends its assertions for the fourth capability
domain (save management):

- tables 8 -> 9, phase cards 5 -> 6
- six content assertions (garlic-savemgr, /dev/pfsmgr, sceFsMountSaveData,
  save-management heading, Phase 5, re-sign) so a later edit cannot silently
  drop that whole section
2026-09-26 16:22:39 +08:00
Songlx516 ede12cb914 docs: the READMEs said the harnesses aren't tracked -- they are now
Follow-up to the .gitignore whitelist. Both READMEs described the frontend
harnesses as living "outside the gitignore whitelist", which stopped being true
one commit ago. Reword both, and add .build/ to the project-layout tree now
that it holds tracked files.
2026-09-26 12:14:18 +08:00
Songlx516 edd1a1c177 chore: track the .build validation harnesses in git
The eight validation scripts live under .build/, which is an otherwise
whitelisted scratch directory. They were not on the whitelist, so they were
tracked nowhere -- a scratch-tree cleanup deletes them permanently, and there
is no copy on the release page either.

- ui_retry_test.mjs, ui_upload_menu_test.mjs -- headless-DOM tests against
  assets/main.js (extract password retry flow; upload menu, i18n key coverage,
  CSS cascade scoping, extract-button state)
- preview_build.py, preview_stub.html, preview_check.mjs -- serve the real
  index.html against a stub API and drive it in headless Chromium (hidden and
  focus state, layout overflow, computed highlight values, wrap thresholds)
- readme_header_render.mjs, compare_html_check.mjs, compare_simple_check.mjs --
  render checks for the README header badges and the comparison pages

Scripts themselves are unchanged; this only stops .gitignore from hiding them.
2026-09-26 12:12:41 +08:00
Songlx516 9d8c49a1d4 docs: correct upstream comparison claims, add a short release-notes template
Three "only this fork has it" claims contradicted upstream's own source:

* Encrypted archives are not fork-only. Upstream's frontend carries a full
  password flow (archive_password_required, prompt(), and it persists the
  password to localStorage), and its helper IS 7-Zip. Moved to "shared".
* Split volumes are not fork-only either: upstream's extension regex already
  matches .001 and its README lists .part01.rar.
* "The helper is ~100 MB" was wrong -- the released asset is 1,017,616 B, and
  the correction reverses the verdict: our single file is 33.7% smaller than
  upstream's two files combined.

Neither claim could have been caught from the READMEs: upstream's never says
the word "password". Same for the mixed-encoding note, which now says what the
fork actually adds (decoding the error text) instead of claiming the mechanism.

Also restored the inherited "rename" feature to both READMEs -- upstream lists
it, we have it, the fork's feature list had dropped it.

Release notes are now a flat "What's Changed" bullet list instead of an essay;
.github/release-notes-template.md records the shape so they stay short.
2026-09-25 14:43:53 +08:00
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
Songlx516 770dcb8a9f docs: mark v1.9.3M as published
The release is out, so every "unreleased / not yet committed" statement in the
docs became false. Updated:

  - CHANGELOG: the artifact header says published and links the release; the
    [v1.9.3M] section carries the release date; the sentence claiming the
    GitHub release was "still v1.9.2" is gone
  - README (both languages): the version line reads v1.9.3M, the "what's new"
    heading loses its "(unreleased)" qualifier, and the note around it links
    the release instead of saying the published binary lacks all of it
  - HANDOVER: current commit / tag / release, the artifact table row (no longer
    "worktree, uncommitted"), the test counts (27 -> 40 retry checks, plus the
    40 menu checks and the 12 headless assertions), and the two section headers
    that said "not committed"
  - DEVICE-TEST: the checklist has now been run and passed, so it is an
    acceptance record rather than a to-do; the rollback pointer also offers the
    v1.9.2 release page
  - FORUM-POST-v1.9.2: the note about two gaps closed in the worktree but not
    yet released now says they shipped in v1.9.3M

Left alone deliberately: the single remaining "the then-unreleased worktree"
phrase in DEVICE-TEST, which states a historical fact about the copy that was
named v1.9.2.
2026-09-24 21:11:40 +08:00
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
Songlx516 3f80eb4692 release: v1.9.2 -- re-cut the release so tag = source = binary
The v1.9.1 tag was a lightweight tag sitting on 9a36c3c, four commits BEHIND
the tree that produced the released ELF, so `git clone --branch v1.9.1` could
not rebuild the published artifact. v1.9.2 is that same tree with the version
literal bumped, which puts the tag back on the commit the binary came from.

Source delta vs the v1.9.1 tag (all of it already shipped inside the v1.9.1
binary -- the tag was just wrong, nothing was missing from the download):

  - src/demangle_stub.c + -Wl,--icf=all: 1,034,328 -> 870,488 B (-15.8%)
    The demangler was only reachable from the uncaught-exception path.
  - upload entry: single-click file/folder pick, plus drag-and-drop upload
  - docs: language switcher in both READMEs

Artifact
  web-file-mgr-v1.9.2.elf
  870,488 B
  sha256 177e90fecf93a0251e83f67884fba4551051be248330b0d70fda8ea732f88e84
  e_machine 0x003e (x86-64 / PS5)

Verification
  - 163 checks (ZIP 108 + RAR 27 + 7z 28), 0 failures; `aeshe` remains the
    known -mhe=on gap
  - the build is reproducible: reverting the two version literals and
    rebuilding reproduces the v1.9.1 ELF byte for byte, and re-applying them
    reproduces this one. That is what pins the byte-level difference between
    the two artifacts down to the version string alone
  - docs/SIZE-OPTIMIZATION.md gains appendix A (artifact-consistency
    verification, incl. the 55,280-byte raw diff explained as linker string
    pool relayout) and appendix B (per-symbol accounting: 628 symbols
    removed, 0 added, 76 ICF fold groups)

Not yet validated on retail hardware.
2026-09-20 17:33:32 +08:00
Songlx516 807d129d8d build: drop the unreachable C++ demangler, fold identical code (-15.8%) 2026-09-20 15:44:30 +08:00
Songlx516 70eaa1027d ui: 上传入口去掉二级菜单,新增拖拽上传
- 上传按钮拆为两个一步入口:主按钮选文件、箭头选文件夹,
  删除 split 下拉菜单(含 .split-menu 样式与 toggleUploadMenu)。
- 单选压缩包时询问一次是否解压,取代原「上传单文件并解压」菜单项;
  识别范围放宽到 7z / 分卷(复用 isExtractableArchive)。
- 新增拖拽上传:webkitGetAsEntry 递归收集文件与文件夹,
  全屏遮罩提示「松开即上传」。
2026-09-20 13:19:20 +08:00
Songlx516 575f94cd39 docs: 英文 README 顶部补语言切换条
之前提交漏掉了 README.md 的切换条,仅中文版有;现补上,两侧互相链接。
2026-09-17 12:14:09 +08:00
Songlx516 2059c0e4b5 docs: 中英文 README 顶部加语言切换条
两份 README 互相链接 English/简体中文;顺手修正英文版版本号 v1.9 -> v1.9.1。
2026-09-16 17:36:22 +08:00
Songlx516 9a36c3cb30 docs: 新增简体中文 README (README.zh-CN.md)
翻译自 README.md,版本号同步为 v1.9.1,并补全 7z 解压章节、修正项目结构。
2026-09-16 16:57:49 +08:00
Songlx516 5e8b56f23e docs: record the multithreaded LZMA2 path and the ZIP fsync removal 2026-09-16 14:31:25 +08:00
Songlx516 5cc493d152 perf: multithread the plain-LZMA2 7z folders via Lzma2DecMt
Most 7z folders are a single plain LZMA2 coder (7-Zip default -m0=lzma2
-ms=on). For that shape the facade now drives the SDK's own parallel
decoder (Lzma2DecMt, vendored with MtDec/Threads) instead of the
single-threaded chain walk; the chain stays responsible for every other
shape (BCJ2 stacks, encrypted folders, exotic methods) and as the
fallback when the platform cannot provide threads (SZ_ERROR_THREAD
downgrades, never fails).

Adapters: ISeqInStream over the read_at callback, ISeqOutStream into the
staging sink (runs on the calling thread, so the sink contract is
unchanged), ICompressProgress polling the cancel hook. The folder CRC is
taken over the decoded stream as before.

329 MiB fixture, internal timing: 1050 ms (asm, 1 thread) -> 930 ms
(4 threads) -> 765 ms (8 threads + 1 MiB inBufSize_MT), i.e. 1.37x,
now at parity with 7za -mmt=off (898 ms); 7za -mmt=8 is 485 ms. The PS5
count is 8 Zen 2 cores like the dev host; SZX_MT_THREADS=8.

Test matrices: 7z 28 + ZIP 108 + RAR 27 checks green.
2026-09-16 14:30:29 +08:00
Songlx516 8766178aa1 perf: stop fsyncing every ZIP entry before its staging rename
The per-entry flush made entry count the dominant cost: an 8000-file
fixture spent most of its wall time in fsync (the extraction phase ran
past 200 s with it, 14.5 s without, same machine). Nothing in the
design needs that durability: a crash mid-extract leaves the staging
tree, which the next run discards, and publish is a rename-only phase.
The RAR and 7z engines never flushed per entry; all three now share
the same policy.

Measured on the same fixture (8000 small files, 1.9 MiB packed):
- with per-entry fsync: >200 s, not finished
- without: 14.5 s, extract phase 14.47 s CPU (scan 0.01 s, publish ~0)
- official 7-Zip on this host: >400 s (real-time AV scans every file
  it creates; the PS5 has no such factor)

ZIP/RAR matrices green (108 + 27 checks).
2026-09-16 14:01:43 +08:00
Songlx516 fd48232b6c perf: enable the SDK's assembly LZMA decoder (1.26x on LZMA2) 2026-09-16 13:07:56 +08:00
Songlx516 49636b0b8a perf: measure our engines against 7-Zip, and record why bash cannot time here 2026-09-16 13:07:55 +08:00
Songlx516 f1633321d2 docs: compare upstream v1.8's extraction design against ours 2026-09-16 13:07:54 +08:00
Songlx516 9058d5a97e docs: record the repo-root cleanup and ship the license audit 2026-09-16 13:07:52 +08:00
Songlx516 5d88914674 docs: rewrite HANDOVER.md for the closed-out 7z + versioning state
The handover doc had grown to 349 lines through accretion: three separate
7z sections that repeated each other's facts, a full Frostpunk 2 debugging
transcript that has been resolved since e0bc4a6, and a "current status" line
still pointing at 00b750d. Anyone picking this up would have to read the
whole thing to work out what is actually left to do.

Rewritten around the questions a new maintainer actually asks:

  * what is the state, in one table, with the exact ELF hash
  * what do I run to build / test (the one-click script, plus the
    /usr/bin/bash caveat and the PIPESTATUS/od-encoding traps)
  * what are the design constraints I must not break (7z coder out_size,
    volstream struct layout, AesOpt.c flags, *at() on PS5)
  * what is left (only -mhe=on, plus real-hardware verification)

History is compressed to the facts that still constrain future work, not
the sequence in which they were discovered. New sections cover the version
plumbing (2.2/2.3), the license/ownership audit result, and the two
trap files sitting untracked in the repo root.

No code changes.
2026-09-15 16:50:00 +08:00
Songlx516 0d036a74a7 feat(ui): source the footer version from /api/version instead of a JS literal
The footer string in the browser was a hard-coded `const APP_VERSION = "v1.9"`
in assets/main.js, completely disconnected from the Makefile's VERSION_TAG.
It had already drifted: the UI kept showing "v1.9" through the entire v1.9.1
release, while the startup notification and the ELF name both said v1.9.1.
Two sources of truth for one fact is one too many.

Backend:
  * new src/version.c -- GET /api/version -> {"ok":true,"version":"v1.9.1",
    "titleId":"FMGR88888"}, built straight from the -DVERSION_TAG /
    -DTITLE_ID macros the Makefile already passes, so there is nothing left
    to forget when bumping a release.
  * registered in filemgr_api_request() next to /api/space and declared in
    filemgr_internal.h; added to COMMON_SRCS so both the PS5 and the linux
    targets link it.

Frontend:
  * the literal becomes APP_VERSION_FALLBACK and is rendered first so the
    footer is never empty, then loadVersion() refreshes it from the API in
    the background. A failed request is deliberately swallowed -- a wrong
    version string is cosmetic and should not raise an error toast.
  * both the host i18n path and the PlayStation browser branch are
    untouched; the footer element itself (#versionText) does not move.

Note the earlier answer to "does the version show in the PS5 UI?" was that it
does -- the bottom-right footer -- but it was showing the stale literal, not
the build's real tag. That is what this fixes.

Verified:
  * WSL prospero-clang 18.1.8 -- ELF 1017864 B, e_machine=0x003e, contains
    both the string "v1.9.1" and the "/api/version" route.
  * MinGW gcc 16.2.0 host tests -- ZIP 108 + RAR 27 + 7z 28 = 163 checks,
    0 failures.
2026-09-15 16:48:53 +08:00
Songlx516 1fa2f0953f build: name the output ELF after VERSION_TAG (web-file-mgr-v1.9.1.elf)
Every build overwrote the same `web-file-mgr.elf`, so the only way to tell two
builds apart was the sha256 -- and the one thing you cannot see from a
filename on a USB stick is which firmware you are about to run. Derive the
binary name from VERSION_TAG:

  BIN       := web-file-mgr-$(VERSION_TAG).elf
  LINUX_BIN := web-file-mgr-linux-$(VERSION_TAG)

Because VERSION_TAG also feeds -DVERSION_TAG, bumping it now updates the
embedded version string, the PS5 notification and the filename in one place.

The build scripts must not hardcode the name or they break the moment the
version changes, so build-elf-wsl.sh now reads it back out of the Makefile:

  VERSION=$(sed -n 's/^VERSION_TAG *[?:]*= *//p' Makefile | head -1)
  BIN_NAME="web-file-mgr-${VERSION}.elf"

rsync exclude widened to /web-file-mgr-*.elf, and build-win.sh now always
re-pushes the WSL script (it only copied it when missing, so editing
build-elf-wsl.sh silently kept running the stale copy).

Verified: same sources as the previous commit produce a byte-identical binary
under the new name -- sha256 94851c26ebe1a3b296fb78748aa8de90ea0c022aa4004d64e700d4156a0cc64a
both before and after, so this commit is pure plumbing.
2026-09-15 13:35:38 +08:00
Songlx516 f4fd464353 build: bump VERSION_TAG to v1.9.1 and whitelist .build/ in .gitignore
The ELF we tagged v1.9.1 still reported "Version: v1.9" -- Makefile line 16
pinned VERSION_TAG by hand and nobody updated it when the tag was cut, so the
PS5 boot notification and `web-file-mgr.elf --version` both mislabeled the
binary. Bump it to v1.9.1 and switch := to ?= so a one-off build can override
it (`make VERSION_TAG=v1.9.2`) without touching the file.

Rebuilt ELF:
  1017864 B, sha256 94851c26ebe1a3b296fb78748aa8de90ea0c022aa4004d64e700d4156a0cc64a
  e_machine=0x003e, `strings` confirms the embedded literal is now v1.9.1

.gitignore: the per-directory deny list for .build/ never kept up -- every
debugging session drops a new probe directory (aespw/, bigzip/, chaincheck.py,
dbg/, engine-e2e/, ...) and they all showed up as untracked. Flip to a
whitelist: ignore .build/* and re-allow only the files that are genuinely part
of the repo. Also ignore .workbuddy/ (session data: never commit, never delete).
2026-09-15 13:24:04 +08:00
Songlx516 5229cd59df docs: .gitignore comment -- list the three tracked .build scripts 2026-09-15 13:16:26 +08:00
Songlx516 a54f34bcab build: add one-click Windows -> WSL ELF build scripts
Two scripts, so building for PS5 is one command:

  /usr/bin/bash .build/build-win.sh

build-win.sh (Windows side, Git Bash)
  Just pipes the WSL script into wsl.exe via stdin and propagates the
  exit code. Uses `wsl.exe -- bash < script` rather than `-- bash -c '...'`
  because the repo path contains spaces and -c's argument gets split.
  Copies the WSL script over first (.build/ is excluded from rsync so
  the WSL side may not have it yet).

build-elf-wsl.sh (WSL side)
  5 stages, each failing loudly:
    1. env check   -- SDK present, prospero-clang runnable,
                      libmicrohttpd in the sysroot
    2. rsync       -- Windows repo -> /home/song/ps5-web-file-manager,
                      incremental, skipping ps5-obj/ gen/ tests/ docs/,
                      then a smoke check that this session's key files
                      landed (sevenz_*.c, zipx_common.c, Makefile, ...)
    3. make all
    4. verify      -- size / sha256 / e_machine, with an explicit
                      aarch64 guard (183) since PS5 is x86-64 (62)
    5. copy back   -- ELF -> Windows project root

make's exit status is read through PIPESTATUS (the `| tail -120`
would otherwise swallow it), so a failed compile actually fails the
script instead of sailing on to the verify stage.

One gotcha worth recording: `od -An -tx2 -j 18 -N 2` prints the
2-byte little-endian *value*, so the on-disk bytes "3e 00" come out
as "003e", not "3e00". Matching on the byte order fails; convert with
$((16#$EM)) and compare numerically (62 = x86-64, 183 = aarch64).

Verified end to end: 1017864 bytes, sha256 2eb04581...473a157,
e_machine 0x003e (62), copied back to the Windows project root.
2026-09-15 13:15:10 +08:00
Songlx516 98679423e0 docs: HANDOVER.md status table -- 六种组合 + 密码 + PS5 构建全部闭合
补充本会话后半部(commit 112f8a6 UX 修补 + 00b750d PS5 真机构建)的进度记录:
  * 第五节任务清单状态表从「进行中/⏳」改为「✅」并补一行「ⓩ PS5 真机构建」
  * 当前矩阵更新为 ZIP 108 / RAR 27 / 7z 28 = 163 checks 全绿
  * 「其他遗留」补「新增 ZIP/RAR/7z 三类分卷的真机验证」「新增加密 7z 的真机验证」
  * 新增「七组合细节」小节列出七组合每个跨引擎不变量
  * 新增「PS5 真机构建已闭环」记录 AesOpt.c 失败 → 不能简单删 → 路径过滤 -maes -mavx2 -mvaes 的完整决策链
  * 新增「7z facade UX 修补」回顾 5 项 i18n + 进度报告回归锁定
2026-09-13 14:14:21 +08:00
Songlx516 00b750d80b build: enable -maes -mavx2 -mvaes for the 7z TU family so prospero-clang accepts AesOpt.c
PS5 (Zen 2) has every AES-NI / AVX / VAES instruction set, but prospero-clang
defaults to a generic x86_64 target that does NOT pre-declare
_mm256_aesenc_epi128 -- AesOpt.c relies on a compiler-version macro that lets
clang 18 in unconditionally, so it blew up with 20 errors and stopped at
-ferror-limit= before the rest of the build could even run.

You can't drop AesOpt.c either: Aes.c references the
AesCbc_{Encode,Decode}_HW / AesCtr_Code_HW[256] symbols via AesGenTables,
so without AesOpt.c the link fails (five undefined-symbol errors).

Fix: add a path-filtered CFLAGS_7Z = -maes -mavx2 -mvaes and apply it only to
third_party/7z/*.c -- zlib and minizip-ng don't need it, the project sources
don't need it, and it's a no-op at runtime since the symbols exist on PS5.

Verified:
  * WSL prospero-clang++ 18.1.8 (FreeBSD sysroot) -- success
    web-file-mgr.elf 1017864 B, sha256 2eb04581...473a157, e_machine=0x003e
  * MinGW gcc 16.2.0 host tests -- 163 checks (ZIP 108 + RAR 27 + 7z 28) all pass
2026-09-13 14:11:35 +08:00
Songlx516 112f8a6b72 fix(7z): keep the spinner alive on small archives; surface password errors with the archive path
UI regression: a 7z with fewer than 4096 entries spun a "scanning archive"
label and never replaced it until extraction had been running long enough
for the byte-level callback to fire, on a slow PS5 that meant "looks hung".
Lower the per-entry barrier in scan_entries to 256 entries, force a final
report after the scan so entries_total / bytes_total always reach the UI,
and force a report at the start of precheck_folders so the throttled
"between scan end and first extraction byte" window is not silent either.

Error regression: when the password was wrong the engine returned
ZIPX_ERR_PASSWORD with an empty detail field; extract_set_error copied it
into task->error_arg verbatim, so the i18n template's {arg} expanded to ""
and the user saw "Wrong password: ". Carry the original archive path into
the error detail so the toast reads "Wrong password: myfile.7z".

Languages: drop the stale "only ZIP/RAR" line from err_extract_unsupported
(7z has been supported since v1.9), and add the new err_extract_password
key in both en/zh.

Tests: lock down both regressions.
  * progress recorder: bytes_done monotonic, total > 0, final byte count
    reaches >= 90% of the declared total.
  * wrong password: r.detail contains the archive basename.

7z 28 + ZIP 108 + RAR 27 = 163 checks, 0 failures.
2026-09-13 10:16:53 +08:00
Songlx516 27eec8c399 docs: HANDOVER.md -- 7z 引擎 + 分卷 + 7zAES + dispatch 全落地 2026-09-13 10:00:07 +08:00
Songlx516 200b38604f feat(7z): dispatch + archive recognition + password UI
With the extraction engine in place (021c9cb), the /api/extract task
finally picks .7z files up.

* src/filemgr_internal.h -- file_task_t gains extract_password (256
  bytes, UTF-8, the size a sane user will never exceed but enough for the
  encrypted RAR / 7z passwords the engine actually accepts).
* src/extract.c -- extract_dispatch() routes a single .7z to sevenz_extract()
  and a byte-split set (name.7z.001, .002, ...) to the same call (the
  volume detector classifies by extension).  api_extract() reads the
  optional "password" form field and copies it into the task; the error
  code map gains ZIPX_ERR_PASSWORD -> "extract_password".
* Makefile -- the sevenz_extract / sevenz_chain / sevenz_volstream sources
  join COMMON_SRCS, and every third_party/7z/*.c (LZMA SDK 26.03, public
  domain, decoder subset) joins THIRD_PARTY_C_SRCS.  The C flag set gains
  -Ithird_party/7z and -DZ7_PPMD_SUPPORT, the latter so PPMd is built
  in (the SDK guards PPMd sources behind a macro that defaults to off).

* assets/main.js -- isSevenZipArchive + isSevenZipSplitVolume; both feed
  isExtractableArchive.  actionExtract() now prompts for a password on
  7z archives (encrypted-stream support is opt-in -- the engine rejects
  unencrypted archives with no password just fine, but the prompt saves
  a wasted scan when an archive is encrypted).  startExtractTask() takes
  the password and sends it as a form field.

* assets/lang-{en,zh}.js -- extractPasswordAsk string for the new prompt.

ZIP 108 / RAR 27 / 7z 26 still all green on the host.

No real-device build attempted in this commit: the WSL runner is the
one that knows the PS5 toolchain, and the user has to be the one to
push it through.  The Makefile changes are the wiring the cross
compiler needs; a `make linux` on a Linux host with libmicrohttpd-dev
should link end-to-end.
2026-09-13 09:59:30 +08:00
Songlx516 021c9cb339 feat(7z): extraction facade + format dispatch + UTF-8 host shim
The 7z chain decoder (commit 2748b38) handles bytes; what the /api/extract
task needed was a third sibling of zip_extract.c and rar_extract.c that
runs that chain over a real archive, mirrors their staging / publish /
rollback / name-validation machinery, and fills in zipx_result_t the same
way so the dispatcher and the web UI can treat every format alike.

* src/sevenz_extract.[ch] -- one pass over the archive (scan: validate every
  name, sum bytes, drop archives the chain cannot drive), then folder by
  folder through the chain.  The publish phase recurses with OVERWRITE and
  MERGE alike for matching directories, and only differs at the file
  leaves -- a strict non-recursive OVERWRITE would have made the policy
  useless for any archive that overlaps a directory already on disk.
* src/zipx_common.c -- the limits profiles and zipx_status_string() that
  the three engines share.  Pulled out of zip_extract.c so every consumer
  gets the same error text and the same MAX_* numbers.
* src/zip_extract.[ch] -- ZIPX_ERR_PASSWORD joined the contract (7zAES).
* tests/run-sevenz-tests.sh -- the engine matrix now also runs the facade
  over every fixture, plus 22 error and policy checks against
  test_sevenz_extract.  KNOWN_GAPS holds only "aeshe" (encrypted header);
  the plain 7zAES fixture is in the matrix.
* tests/test_sevenz_extract.c -- end-to-end driver for the facade.

The POSIX shim the host test runner injects needed three more pieces so
the tests pass on a CP936 host:

* lstat/stat -> wfm_stat (the MinGW ANSI entry points can't see a UTF-8
  filename in CP936; without the redirect a non-ASCII entry causes the
  publish phase to lstat() a mangled path and fail with ENOENT).
* opendir/readdir/closedir -> the wide variants, so readdir() hands us
  real UTF-8 names instead of CP936 bytes that nothing can round-trip.
* fopen -> _wfopen, for the test helpers that read a non-ASCII file
  back to verify it.

ZIP 108 checks / RAR 27 checks / 7z 26 checks, all green; the AES
fixture extracts with the password "Secret123" and rejects wrong / absent
passwords with "password required or wrong" / "wrong password, or the
archive is damaged".

tests/fixtures/ gains the volume-set fixtures (broken.zip.001, gap.zip.*,
disks.*, parts.part*.zip, plain.zip.*, split_single.zip) that were
generated by tests/make_split_fixtures.py in earlier sessions but never
made it into the index.

Next commit wires sevenz_extract() into the dispatch in src/extract.c and
recognises 7z archives in assets/main.js.
2026-09-13 09:52:44 +08:00
Songlx516 4da345a6e8 feat(7z): decrypt 7zAES packed streams
7-Zip wraps the packed stream of a password protected archive in a 7zAES
coder (method 0x06F10701).  The bundled LZMA SDK has no C implementation of
it, so add one to the folder chain:

  * aes_props_layout() splits the property bytes into numCyclesPower, salt
    and IV exactly as CDecoder::SetDecoderProperties2() does, and rejects a
    block whose declared lengths do not match its size;
  * aes_derive_key() runs the 7-Zip KDF: SHA-256 over
    (salt || password_utf16le || counter_le64) repeated 1 << numCyclesPower
    times, with the 0x3F power meaning "no derivation", the key being the
    salt and password copied into 32 bytes;
  * utf8_to_utf16le() converts the caller's password, since that is the
    encoding 7-Zip hashes;
  * the SZ_N_AES node decrypts one block at a time with Aes_SetKey_Dec() /
    AesCbc_Init() / g_AesCbc_Decode(), keeping the trailing partial block in
    the node so the declared (unpadded) unpack size is what gets delivered.

sz_chain_decode() gains a `password` argument, and sz_chain_needs_password()
lets a caller ask for one before it touches the filesystem.  A folder that
needs a password and did not get one fails as SZ_CHAIN_ERR_PASSWORD with a
message naming 7zAES, rather than as a generic corrupt archive.

Because a wrong password decrypts to plausible looking rubbish, a failure
from a folder that carried a 7zAES coder gets "(a wrong password looks like
this)" appended, so the UI can offer a retry instead of calling the file
damaged.

numCyclesPower comes from the archive and drives 2^n SHA-256 passes, so it is
capped by the limits profile (max_aes_cycles, 2^24 for both the default and
the large profile).

An encrypted *header* (-mhe=on) is a different problem: the archive header is
encrypted with the same coder and has to be decrypted before any folder
exists.  That stays out of scope and is reported as such.

tests/run-sevenz-tests.sh: aes leaves KNOWN_GAPS and passes; aeshe stays
there with the reason spelled out.
2026-09-13 09:13:04 +08:00
Songlx516 9b2f5a07c3 feat(7z): read multi-volume 7z sets as one stream
A split 7z (`name.7z.001`, `.002`, ...) only contains the first slice of the
archive, so SzArEx_Open() followed the header offsets off the end of the file
and gave up with SZ_ERROR_INPUT_EOF -- the set was unusable even though the
file listing of each part looked fine.

src/sevenz_volstream.c backs ISeekInStream with the ordered part list, so the
SDK sees one continuous archive and never learns it was split. Nothing is
merged on disk: a 160 GiB set would otherwise need a second 160 GiB scratch
copy. A 7z split is a plain byte split, so offsets stored inside the archive
are already absolute and the Stream/Seek callbacks stay trivial.

The ordered list comes from zipx_volume, which already understands
`name.7z.001` naming, so an incomplete set fails before any decoding starts
and names the missing part. Two further checks report a set that could not
have worked anyway: a lone first volume, and a part whose size differs from
the earlier ones (7-Zip cuts equal sized parts and lets only the last one be
short).

To reuse the split-layout constants without dragging the ZIP stream header
into the 7z build, ZIPX_VOL_MODE_* moved to zipx_volume.h: they describe the
set, and every consumer of zipx_volume_t needs them.

The e2e driver now opens its input through the volume stream, so the packed
data reads go through it too.

Matrix: all 10 single-volume fixtures plus vol.7z.001 are byte-identical
(13 passed, 0 failed). Remaining known gaps: 7zAES (aes, aeshe).
Regression: ZIP 108 / RAR 27 checks, 0 failures.
2026-09-12 17:53:17 +08:00
Songlx516 2748b383bd feat(7z): decode folders with our own folder header parser and coder chain
The LZMA SDK's CSzFolder caps a folder at four coders, so anything 7-Zip
writes for BCJ2 (4 packed streams + 5 coders) comes back as
SZ_ERROR_UNSUPPORTED from SzAr_DecodeFolder. The header scanner has no such
limit (64 coders), which makes the failure look like corrupt data instead.

src/sevenz_chain.c parses the folder descriptor itself and drives the codec
chain as a pull pipeline: every decoder hands up to N bytes to its consumer
and asks upstream for more when it needs it, so a folder is decoded straight
into the caller's sink without staging the whole thing. Covered here:

  - Copy / LZMA / LZMA2 / PPMd, Delta, and the x86, PPC, IA64, ARM, ARMT,
    SPARC branch converters
  - BCJ2, whose three side streams have to be materialised while MAIN keeps
    streaming
  - per coder output sizes taken from UNPACK_INFO, which is what keeps the
    chains exact

tests/sevenz_chain_e2e.c decodes each folder once and splits the byte stream
across the entries that share it, mirroring the real extraction path, and
checks the per-entry and per-folder CRCs on the way past.
tests/make_sevenz_fixtures.py builds the fixture matrix (including
non-solid `-ms=off` folders), and tests/run-sevenz-tests.sh runs it while
keeping a KNOWN_GAPS list that fails loudly once a gap closes.

Two real bugs surfaced while bringing the matrix up:

  - build_coder() gave every coder node the *folder's* unpack size instead of
    its own entry in UNPACK_INFO. In a BCJ2 folder MAIN is regularly larger
    than the folder's final size (300066 vs 300000 in the fixture), so the
    node silently truncated its own output and the chain came up short by 21
    bytes per LZMA2 layer. Only archives where a coder is bigger than the
    folder tripped it, which is why single-folder BCJ2 passed and the same
    archive split into per-file folders did not.
  - the e2e driver kept `written` across folders, so a folder that failed
    mid-entry made every later entry look half-written and the failures
    cascaded.

Scripts no longer delete anything: objects are overwritten and each case gets
a fresh output directory, so the suite runs under a guarded shell.

Matrix: 10/10 single-volume fixtures byte-identical (store, lzma2, lzma,
ppmd, bcj, delta, utf8, bcj2, solidoff, bcj2off). Still open and listed as
known gaps: 7zAES (aes, aeshe) and multi-volume input (vol.7z.001).
2026-09-12 17:46:32 +08:00
Songlx516 8bee84bd49 feat(7z): vendor LZMA SDK and add the 7z fixture/verification harness
Groundwork for 7z support (the second of the six format x volume
combinations). No engine code yet: this lands the vendor subset, real
fixtures and the end-to-end driver, so the engine has something to be
built and checked against.

Vendor (third_party/7z, LZMA SDK 26.03, public domain):

- decoder-only subset: 7zArcIn/7zDec container, Lzma/Lzma2/Ppmd7/Bcj2/
  Bra/Delta codecs, Aes/Sha256 for the password channel to come
- the encoder half (LzmaEnc, Xz*, Sort, Threads, ...) is deliberately
  not vendored; the engine never encodes
- README.md records two SDK limitations that shape the engine design:
  * CSzFolder caps a folder at 4 coders / 3 bonds, so 7-Zip's own BCJ2
    chain (BCJ2 + 4xLZMA2 = 5 coders) is refused by SzAr_DecodeFolder
    even though SzArEx_Open parses it happily - the engine therefore
    parses folder blobs and drives the codec chain itself
  * the C decoder ships no 7zAES coder at all, so both -p and -mhe=on
    archives are rejected until the engine implements it on top of the
    vendored Aes.c / Sha256.c

Tests:

- make_sevenz_fixtures.py builds real archives with an actual 7-Zip
  binary (7za from the Extra package; falls back to the reduced 7zr,
  which has no PPMd encoder): store/lzma/lzma2/ppmd/bcj/delta/bcj2,
  AES with plain and with encrypted headers, a 7-part -v100k volume
  set, and UTF-8 entry names
- sevenz_e2e.c extracts an archive and validates every entry against
  the stored CRC; it calls the wide-character file APIs on Windows, so
  non-ASCII entry names are really created instead of silently failing
- run-sevenz-tests.sh builds the subset and the driver, then runs the
  fixture matrix with an explicit KNOWN_GAPS list that fails loudly
  when one of the gaps starts passing

Status: store, lzma, lzma2, ppmd, bcj, delta and utf8 all extract
byte-identical to the source tree (7/10). bcj2, aes, aeshe and
vol.7z.001 are the known gaps, each mapped to a specific piece of
engine work still to come.
2026-09-12 17:17:45 +08:00
Songlx516 da565ccb7c feat(zip): extract multi-file ZIP volume sets
A split ZIP is several files that only form an archive together. Until now a
`.zip.001` was rejected as an unsupported format and a `.z01`/`.partN.zip`
member could not be resolved, so these downloads were unusable.

Three naming conventions are recognised, from any member of the set:

  name.zip.001, name.zip.002, ...   byte split (7-Zip)
  name.part1.zip, name.part2.zip    byte split (WinRAR)
  name.z01, ..., name.zip           zip split disks (Info-ZIP / PKZIP)

The parts are served through a new stream that makes them look like one
archive, so nothing is copied to disk first (a 160 GiB set would otherwise
need a second 160 GiB scratch copy):

- src/zipx_volstream.c implements an mz_stream over the ordered part list.
  Byte splits keep absolute offsets; zip split disks carry offsets relative to
  the disk an entry starts on, so that mode honours
  MZ_STREAM_PROP_DISK_NUMBER the way mz_zip_entry_seek_local_header drives it
  and starts on the last volume, where the central directory lives.
- src/zipx_volume.c groups the set, verifies the numbering is complete and
  reports exactly which volume is missing.
- zip_extract.c tries the layout implied by the naming first and the other one
  as a fallback, so tools that disagree about offset bases still work.
- extract.c dispatches volume members to the engines and, when the caller
  asked for the source to be deleted, removes every volume instead of leaving
  orphaned parts behind.

Error messages are specific on purpose: an incomplete set reports the missing
file name (e.g. "gap.zip.002 is missing"), a lone first volume says the
remaining volumes are missing, `.rar.001` sets explain the rename that makes
them work, and 7z volumes say 7z is not supported yet.

Tests: tests/make_split_fixtures.py builds all three layouts (the disk set is
written with per-disk offsets exactly as APPNOTE 4.4.11 describes) plus two
broken sets; test_zip_extract.c extracts each layout from a different member
and byte-compares the nested binary entries. 108 ZIP + 27 RAR checks, 0
failures.
2026-09-12 16:13:20 +08:00
Songlx516 01e27f3825 fix(rar+zip): byte-accurate RAR progress; stop false "compression bomb" on small entries
Two issues found in real-machine testing of v1.9:

1. RAR multi-volume extraction never advanced the progress bar. The engine
   only accounted bytes_done after RARProcessFile() returned for a whole
   entry, so a multi-GB entry spanning volumes froze the UI for its entire
   duration. Wire RARSetCallback/UCM_PROCESSDATA, which unrar fires per
   decompressed chunk during disk extraction, and fold each chunk into
   bytes_done + a throttled progress report. (DLL-mode break handling is
   never armed, so cancellation stays entry-granular; returning -1 would
   be ignored.)

2. A 95k-file game zip was refused as a "compression bomb" because one
   small entry exceeded the per-entry uncompressed/compressed ratio cap
   (500/1000). Such entries are common and harmless in legitimate archives
   (zero-filled placeholders, sparse blobs): actual bytes written are
   bounded by the declared size (enforced during extract) and check_space()
   verifies the full declared total against real free space before any
   writes. Ratio screening now only applies to entries >= 1 GiB
   (ratio_min_bytes, default in both profiles); sub-GiB high-ratio entries
   are accepted. Error text now includes compressed -> uncompressed sizes.

Tests updated: small high-ratio bomb.zip now extracts OK under both
profiles and is rejected again when ratio_min_bytes is lowered below its
size; RAR multi-volume fixture asserts mid-entry progress events and that
final bytes_done reaches bytes_total. 73 ZIP + 27 RAR checks, 0 failures.
2026-09-07 22:25:11 +08:00
Songlx516 e0bc4a6ae0 fix(zip): fall back to plain path calls when *at() fails at runtime
Real-machine diagnosis of the Frostpunk 2 failure: mkdirat() returns -1
with errno 0 ("cannot create directory ... (No error)") on PS5 hardware.
The *at() symbols link against the SDK libc but misbehave at runtime,
while plain path-based calls work (the staging root open() succeeds and
small flat archives extract fine).

Every *at() call in the extract phase now retries as a full-path call
built from the staging root before reporting an I/O error:

- open_parent_dirs: mkdirat -> mkdir, openat -> open
- write_entry: openat -> open, renameat -> rename, unlinkat -> unlink

Error messages now append (errno=%d) so errno=0 can no longer hide
behind a misleading "No error" strerror string. Host suite stays green:
94 checks, 0 failures.
2026-09-06 22:25:09 +08:00
Songlx516 f820016de3 test(host): fix statvfs shim for big-file e2e; add standalone bigfile driver
Host-side verification that the ZIP engine can extract a >4GiB zip64
entry end to end (4.7 GiB fixture, content byte-compared):

- tests/compat/sys/statvfs.h: resolve the path before taking the drive
  letter (relative paths used to fail) and scale f_bavail by 4096 since
  `unsigned long` is 32-bit on Windows and silently truncated ~250GB
  down to ~2.3GB; real 64-bit hosts and the PS5 (LP64) are unaffected.
- tests/bigfile_e2e.c: standalone driver to extract one large archive
  and print the engine result (verification via cmp/sha256 by caller).
- tests/run-tests.sh: incremental clean instead of `rm -rf` of the whole
  build tree.
- Makefile: force -DHAVE_FSEEKO so minizip uses fseeko/ftello instead of
  falling back to fseek/ftell on libcs without the probe.

Result: 4.7 GiB zip64 extracts OK on host in ~6s, output identical to
source. Engine 64-bit write path confirmed clean; PS5 failure needs the
strerror passthrough build for real-machine errno capture.
2026-09-06 09:41:32 +08:00
Songlx516 95578fb2d2 test(zip): build host minizip with -D_FILE_OFFSET_BITS=64
Host (MinGW) tests default to 32-bit off_t, so minizip lseek overflowed
on the >4 GiB zip64 fixture ('Invalid argument' opening big.zip). The PS5
Makefile already defines _FILE_OFFSET_BITS=64; this makes the host engine
matches production and lets the >4 GiB path actually be tested here.
2026-09-05 23:55:11 +08:00
Songlx516 4695295b8e fix(ui): surface backend strerror detail in error toasts
err_extract_io / err_extract_crc / err_extract_open_failed etc. only showed
the entry name (detail); the backend message carrying strerror (e.g. 'No
space left on device', 'File too large') lived in task.error but was hidden
by the i18n label. backendErrorText now appends the backend message when an
i18n label exists and the fallback differs from the generic default.
2026-09-05 23:53:36 +08:00
Songlx516 76c4eadbf5 docs(changelog): fill v1.9 ELF sha256 (bb8f17e9...) 2026-09-05 23:34:36 +08:00
Songlx516 ad765776f7 fix(build): stub libgcc __cpu_model symbols for PS5 link
unrar rijndael/system call __builtin_cpu_supports (AES-NI probe) which
clang lowers to __cpu_model + __cpu_indicator_init; the FreeBSD-style PS5
sysroot ships no libgcc, so the link failed. Add a PS5-only (x86_64, non
linux/win) stub TU; host builds keep the real libgcc-provided symbols.
2026-09-05 23:34:35 +08:00
Songlx516 ae41434089 fix(build): restore POSIX type shims in unrar_c_api.h
The _WIN32-only include of windows.h left PS5/prospero builds (no _WIN32,
no _UNIX for project sources) with PASCAL/HANDLE/LONG undeclared when
rar_extract.c pulled in dll.hpp. Restore the _UNIX mirror block that was
lost in an earlier edit.
2026-09-05 23:31:38 +08:00
Songlx516 af6b440f33 fix(build): compile project C sources with -x c when linking via clang++
The link driver is now the C++ compiler (needed for unrar objects), but
it treats the .c files on the command line as C++ and clang 18 rejects
that with -Werror=deprecated. Pass -x c before the C source list and
-x none before the object files.
2026-09-05 23:30:03 +08:00
Songlx516 01a6616472 fix(build): exclude Windows-only unrar sources on PS5/linux; abort on make fail
isnt.cpp / motw.cpp need windows.h (OS version / Zone.Identifier checks)
and are not in the official unrar UNIX makefile; the _UNIX branch
#ifdef's their symbols out, so PS5 and linux builds must not compile
them (first PS5 build failed on isnt.cpp). run-tests.sh MinGW flow
keeps them.

build-elf.sh: make failure previously fell through to step 7 which
reported the STALE elf as OK (sha unchanged, user confused); make now
aborts the script (pipefail already set).
2026-09-05 23:26:37 +08:00
Songlx516 068983b062 release(v1.9): docs, notices and version bumps (ELF sha TBD until WSL build)
- CHANGELOG: new [v1.9] section (engine swap rationale, v6 / multi-volume /
  decryption-capable, 94 checks); banner with TBD size/sha256.
- README: header version v1.9; "What's new in v1.9"; RAR extraction section
  rewritten for unrar 7.20.1 (v6 + multi-volume supported, encrypted pending
  password plumbing); vendoring/licence and old v1.9-plan sections replaced.
- THIRD_PARTY_NOTICES: dmc_unrar GPL-2.0 entry -> rarlab UnRAR freeware
  licence entry (third_party/unrar7/license.txt); dmc_unrar history note.
- assets/main.js APP_VERSION "v1.8.3" -> "v1.9" (footer version string).
- Makefile VERSION_TAG v1.8 -> v1.9.
2026-09-05 23:23:21 +08:00
Songlx516 da67bfa284 test(rar): commit real RAR fixtures (v6/encrypted/3-volume)
Generated once with tests/make-rar-fixtures.bat (WinRAR RAR 7.23) so
checkouts without WinRAR still exercise the real-archive happy paths.
basic-rar4.rar is absent because RAR 7.x dropped RAR4 writing; that
check SKIPs.
2026-09-05 23:18:39 +08:00
Songlx516 452b9b0187 fix(extract): handle multi-volume split segments in scan and extract
unrar in RAR_OM_EXTRACT mode presents a file that spans volumes as
several header segments with the SAME name, flagged RHDF_SPLITBEFORE/
SPLITAFTER. The scan dedup rejected the continuation segments as
"duplicate entry name" and the whole multi-volume extract failed.

Fix: segments after the first (RHDF_SPLITBEFORE set) skip the
normalize/dedup/size/limit accounting in scan (the first segment already
carries the full size) and, in extract, still call RARProcessFile(EXTRACT)
to drive the volume chain but are not counted or size-accumulated.

Verified against real fixtures (tests/fixtures-real/, generated by
tests/make-rar-fixtures.bat on the user's RAR 7.23): a 512 KB file split
across vol.part1/2/3.rar extracts whole (status ZIPX_OK, big.bin + root.txt).
Host suite now 70 ZIP + 24 RAR checks, 0 failures.
2026-09-05 23:18:18 +08:00
Songlx516 91757bfd02 fix(tests): preserve dir structure in rar fixtures (-ep1 was flattening)
-epl + absolute stage paths stored dir\nested.txt as nested.txt, so the
v6/volume happy-path checks found no dir\nested.txt. Generator now cd's
into the stage dir and passes relative names without -ep1.
2026-09-05 23:08:41 +08:00
Songlx516 30d0decf58 test(rar): keep real fixtures in fixtures-real/ (make_fixtures wipes fixtures/)
tests/make_fixtures.py rmtree()s tests/fixtures on every run-tests.sh
invocation, silently deleting the real RAR fixtures. The WinRAR generator
now writes tests/fixtures-real/; run-tests.sh passes it as a third
argument to test-rar-extract, which resolves v6/volume/encrypted checks
against it and falls back to SKIP when absent.
2026-09-05 23:05:37 +08:00
Songlx516 22a4b5c76a fix(tests): name volume target vol.rar so rar yields vol.partN.rar
Naming the target vol.part1.rar made RAR 7 append its own numbering onto
the whole stem -> vol.part1.partN.rar. Plain vol.rar splits into the
standard vol.part1/2/3.rar the test expects.
2026-09-05 23:02:01 +08:00
Songlx516 ff010add68 fix(tests): delete stale vol files before re-splitting
rar a updates an existing archive instead of re-splitting it, so the
single-volume vol.part1.rar left by earlier runs (small payload) was
grown to 512KB without ever producing part2/3. Remove vol.part* first.
2026-09-05 23:01:17 +08:00
Songlx516 68056f2d9f fix(tests): force real multi-volume fixture with 512KB stored payload
The earlier -v200k split never produced part2 because the payload was a
few hundred bytes. Now generates a 512KB incompressible file (fsutil +
-m0 store) so vol.part1/2/3.rar actually exist for the volume test.
2026-09-05 22:59:21 +08:00
Songlx516 3db921e408 fix(tests): make-rar-fixtures.bat pure ASCII + RAR4 tolerant
Write tool wrote UTF-8 Chinese comments which cmd.exe parsed under the
ANSI codepage as garbage ('M' is not a command). This version is pure
ASCII with CRLF line endings. RAR4 (-ma4) creation fails on newer RAR
builds; that step now warns and continues instead of aborting the whole
run. Requires no behavioural change from the engine side.
2026-09-05 22:58:10 +08:00
Songlx516 c58c145733 test(rar): real-archive fixtures generator + v1.9 happy-path checks
tests/make-rar-fixtures.bat produces real RAR fixtures on a machine with
WinRAR (Rar.exe) so the RAR engine is finally exercised against genuine
archives instead of 11-byte placeholders (v1.8 shipped without ever
running a real RAR through the happy path):
  - basic-v6.rar     RAR5 with WinRAR 6/7 "v6" compression (the format
                     dmc_unrar could not decode — the v1.9 trigger)
  - vol.part1+2.rar  multi-volume (200 KB split)
  - enc-v6.rar       encrypted (password secret123)
  - basic-rar4.rar   legacy RAR4

tests/test_rar_extract.c gains test_real_archives(): asserts v6 single
volume and RAR4 extract OK with expected files on disk, multi-volume
auto-merges from the same directory, and encrypted archives are rejected
up front with ZIPX_ERR_UNSUPPORTED (password channel lands separately).
Each check SKIPs cleanly when the fixture is absent, so plain CI
checkouts stay green.

Run: tests/make-rar-fixtures.bat   (WinRAR required), then rerun
tests/run-tests.sh to flip the SKIPs into real assertions.
2026-09-05 22:53:23 +08:00
Songlx516 a0482a41ed feat(extract): swap RAR engine to unrar 7.20.1 (v1.9, v6/multi-volume)
Replace the dmc_unrar 1.7.0 backend (third_party/unrar/) with the rarlab
UnRAR 7.20.1 source vendored in third_party/unrar7/ (commit c68c0de),
driven through its C-compatible DLL API.

Why: dmc_unrar only dispatches RAR5 compression version 5
(switch(file->version) case 0x5000). WinRAR 6.x/7.x archives (algorithm
"v6") land on the default branch -> DMC_UNRAR_FILE_UNSUPPORTED_VERSION
-> user-visible "corrupt archive", confirmed on a real v6:8M archive on
2026-09-05. unrar 7.20.1 natively handles v6, multi-volume (.partNN.rar
auto-merge) and encrypted archives.

Engine changes (src/rar_extract.c):
- Facade include swapped to third_party/unrar7/unrar_c_api.h; all calls
  now go through RAROpenArchiveEx/RARReadHeaderEx/RARProcessFile/
  RARCloseArchive. scan + extract each re-open the archive (the DLL API
  is sequential, unlike dmc's index-based access).
- Error translation rewritten for the ERAR_* code set.
- extract no longer builds each staging path by hand (open_parent_dirs/
  extract_one removed): unrar writes the whole tree under the staging
  root; names were validated during scan.
- Bug fix: normalize_name() cleared the caller-provided *is_dir (it set
  *is_dir = 0), turning directory entries into files and tripping the
  duplicate detector whenever an explicit directory header followed
  files beneath it (real v6 archive had exactly this). RHDF_DIRECTORY
  is now honored end to end.
- Encrypted RAR still fails up front (ZIPX_ERR_UNSUPPORTED): unrar can
  decrypt, but password plumbing (API/UI) lands in a later commit.

Build:
- Makefile: C++ compiler vars (CXX/HOST_CXX, prospero-clang++ defaults
  to -stdlib=libc++), unrar7 RARDLL source set (49 files, mirrors
  UnRARDll.vcxproj), .cpp pattern rules, link through the C++ driver.
- tests/run-tests.sh: compiles unrar7 objects with $CXX, links the RAR
  test with g++ and -lpowrprof on Windows.
- third_party/unrar/ (dmc_unrar) removed; see git history.

Verified on host (MinGW): full engine run on the user's real v6 archive
(24.9 MB, 170 headers) -> ZIPX_OK, 170/170 entries, 133 files + 37 dirs,
71,514,931 bytes. Host suite: 70 ZIP + 14 RAR checks green.

Frontend password/multi-volume UX and release plumbing (CHANGELOG,
THIRD_PARTY_NOTICES, README, tag) follow in the v1.9 release commit.
2026-09-05 22:40:58 +08:00
Songlx516 c68c0def35 vendor(unrar): add unrar 7.20.1 (RARLab source, opello mirror)
New RAR engine for v1.9, replacing dmc_unrar 1.7.0. dmc_unrar only
dispatches RAR5 compression version 5 (switch(file->version) case
0x5000); WinRAR 6.x/7.x writes version 6 -> DMC_UNRAR_FILE_UNSUPPORTED_VERSION
-> user-facing "corrupt archive" on real v6 archives (diagnosed 2026-09-05
from a v6:8M:m0:m3 test file). unrar 7.20.1 natively supports v6,
encrypted and multi-volume RAR.

Vendored into third_party/unrar7/ (new dir) so the tree stays buildable
while src/rar_extract.c still references the dmc facade. Contents are a
verbatim copy of opello/unrar @97e1780 (159 files, v7.20.1) plus two
project files:
  - unrar_c_api.h  (C facade shim: platform types + dll.hpp re-export)
  - VENDORED.md    (source pin, build model, license notes)

Build model verified on host (MinGW g++ 16.2, Windows):
  - all 49 UnRARDll.vcxproj sources compile with
    -std=c++17 -DRARDLL -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE
  - links + runs: RARGetDllVersion() = 9 (smoke binary in .build/, ignored)
  - Windows link additionally needs -lpowrprof

License: UnRAR freeware license (not GPL) - free to use for handling RAR
archives; cannot be used to build a RAR-compatible archiver. THIRD_PARTY_NOTICES
update lands with the engine swap commit.

Next (v1.9 work items): engine swap in rar_extract.c, Makefile .cpp rules,
fixtures + host tests, frontend password/multi-volume, then ELF build.
2026-09-05 21:57:15 +08:00
Songlx516 8344b9bae0 fix(ui): APP_VERSION now reads v1.8.3 (was hardcoded "v1.7")
versionEl (page footer) showed "v1.7" regardless of the actual build
because APP_VERSION was hardcoded when the frontend was imported in
v1.7 and never bumped through v1.8 / v1.8.1 / v1.8.2 / v1.8.3. Users
running a v1.8.x ELF could not tell from the UI which build they were
on — which made the "no extract button for .rar" report hard to
diagnose (the user's console was still v1.7, which predates RAR file
recognition entirely).

Note: Makefile VERSION_TAG stays "v1.8" (it is a separate C-side macro
not wired to frontend assets); APP_VERSION is the user-visible string.
2026-09-05 21:29:21 +08:00
Songlx516 cb82ee439d docs(changelog): v1.8.3 also ships the build-script rebuild-detection fix
5-file delta summary now mentions the build pipeline fix; new "Build
pipeline" subsection under v1.8.3 explains why and what users (running
the WSL build script) need to do.

ELF sha256 is still TBD until the WSL build is re-run with the patched
script; will fill in when the user pastes the new artifact metadata.
2026-09-05 20:30:16 +08:00
Songlx516 9e9830a214 fix(build-elf.sh): include assets/* and gen-asset-module.py in change check
The step 6 needs-rebuild loop only watched src/*.c, Makefile, and
minizip/zlib headers. Frontend assets (assets/main.js, assets/index.html,
lang-*.js) feed gen/*.c via gen-asset-module.py, so v1.8.3 (which touched
no backend, only assets/) was silently classified as "no source change"
and make was skipped. The ELF sha256 stayed at the v1.8.2 build.

Fix: also iterate assets/* and gen-asset-module.py. Anyone changing a
frontend file (and nothing else) will now correctly trigger a rebuild.

This is the script-side sidekick to commit 687ef6b (v1.8.3 hotfix), which
fixed the actual user-visible bug; this commit fixes the build pipeline
so future frontend-only changes produce a new ELF.

Note: .build/build-elf.sh on Windows is the canonical source. The WSL-side
/home/song/build-elf.sh needs to be re-copied from it before the next build
(re-run "cp /home/song/ps5-web-file-manager/.build/build-elf.sh /home/song/build-elf.sh").
2026-09-05 20:11:55 +08:00
Songlx516 687ef6b297 feat(upload): "Upload and extract" now accepts .rar (v1.8.3 hotfix)
Server-side dispatch in src/extract.c:79 already routes .rar to
rar_extract(), but the upload + extract entry point was hard-coded
to accept only .zip via two layers:

  - assets/index.html:71 <input accept=".zip,application/zip,...">
  - assets/main.js:2340    if (!/\.zip$/i.test(file.name)) return;

v1.8.3 relaxes both to also accept .rar (and the two RAR MIME types),
plus a small i18n pass so the "uploading ZIP"/"ZIP body is large"
copy reads sensibly when the user picks a RAR. No backend, no engine,
no vendored changes.

What this enables today (v1.8.3):
- Upload a single-volume plaintext .rar in one click and have the
  existing rar_extract() engine process it. Uploaded archive is
  auto-deleted on success, same as .zip since v1.7.

Still NOT enabled (planned for v1.9.0):
- Multi-volume RAR (name.part01.rar + name.part02.rar + ...)
- Encrypted RAR (password-protected headers / entries)

Both still return extract_unsupported from the existing
third_party/unrar/dmc_unrar 1.7.0 (GPL-2.0) engine. v1.9.0 will
swap the vendor to opello/unrar 7.20.1 (UnRAR License) which
natively handles both.

Files (4):
  assets/index.html:        accept=".zip,.rar,application/zip,
                             application/x-zip-compressed,
                             application/vnd.rar,
                             application/x-rar-compressed"
  assets/main.js:           upload pre-check regex /.(zip|rar)$/i
  assets/lang-en.js:        "ZIP will be deleted" -> "archive will be deleted"
  assets/lang-zh.js:        upload ZIP / volume ZIP wording neutralized
  CHANGELOG.md:             v1.8.3 entry added above v1.8.2

84 host-side checks (70 ZIP + 14 RAR), 0 failures.
2026-09-05 20:04:11 +08:00
Songlx516 36ea055e70 docs(readme): sync numbers to v1.8.2; add v1.8.1 + v1.8.2 sections
README had several stale values from v1.8.1 that never made it into
the published docs (because v1.8.1 was a hotfix and the README rewrite
focused on v1.8 RAR). v1.8.2 surfaces all of them in one pass.

- Header version tag: v1.8 -> v1.8.2
- ZIP extraction limits table: 1 TiB / 256 GiB -> 2 TiB / 512 GiB
  (large profile: 2 TiB / 1 TiB -> 4 TiB / 1 TiB)
- RAR extraction limits table: same v1.8.2 numbers
- Frontend threshold snippets: 240 GiB -> 480 GiB (both ZIP and RAR)
- ELF size mentions: ~418 KiB / ~430 KiB -> ~497 KiB / ~427 KiB
- Test count: "83 checks (69 ZIP + 14 RAR)" -> "84 checks (70 ZIP + 14 RAR)"
- Makefile layout comment: VERSION_TAG v1.8 -> v1.8.2

Added two new "What's new in" sections so the changelog surface in
README matches the tag history:
- v1.8.1 — default limits 64 GiB -> 256 GiB (PS5 system-backup scenario)
- v1.8.2 — default limits 256 GiB -> 512 GiB (3A-game single-file
  scenario) + the two PS5-only build fixes discovered by the v1.8.2
  cross-compile (Makefile -Ithird_party/unrar, src/extract.c reorder
  for clang 18 -Werror=implicit-function-declaration) + the v1.8.2
  release artifact sha256.
2026-09-05 16:21:15 +08:00
Songlx516 2a346d694c relax(extract): default limits 256 GiB -> 512 GiB / 1 TiB -> 2 TiB; large 2 TiB -> 4 TiB; threshold 240 GiB -> 480 GiB (v1.8.2)
A 3A-game archive with a single ~300 GiB uncompressed file was silently
rejected by the v1.8.1 default profile (scan_archive returns
ZIPX_ERR_LIMIT_FILE_SIZE before the request ever reaches the frontend
confirmation prompt, so the user just sees "卡壳"). The 256 GiB default
cap was tuned for PS5 system backups (many smaller entries) and was wrong
for the 3A-game single-file case.

Limits changes:
- src/zip_extract.c k_default_limits: 1 TiB / 256 GiB / 500 -> 2 TiB / 512 GiB / 500
- src/zip_extract.c k_large_limits:   2 TiB / 1 TiB   / 1000 -> 4 TiB / 1 TiB / 1000 (must stay > default)
- assets/main.js LARGE_FILE_THRESHOLD_BYTES: 240 GiB -> 480 GiB
- assets/lang-{en,zh}.js extractLargeAsk copy reflects new numbers
- tests/test_zip_extract.c: large.max_total_bytes advertised-number assertion updated to 4 TiB
- README.md / docs/HANDOVER.md: limit tables, FAQ, threshold snippets updated
- CHANGELOG.md: v1.8.2 entry added above v1.8.1

PS5-only build fixes (caught by WSL cross-compile, host tests pass without):
- Makefile CFLAGS: add -Ithird_party/unrar so src/rar_extract.c can find
  the dmc_unrar_api.h facade header (THIRD_PARTY_CFLAGS already had it
  but the main CFLAGS for project sources did not).
- src/extract.c: move extract_progress() definition before
  extract_dispatch() so the implicit function declaration is not flagged
  by -Werror=implicit-function-declaration (clang 18 in the PS5 SDK is
  stricter than the host gcc used by tests/run-tests.sh).

Release artifact: web-file-mgr.elf 509 704 bytes,
sha256 1b2c3d68b35e32737105f17d14a80a3c159ceca0cabd274ee168cbcd81906f65,
e_machine 0x003e (x86_64-sie-ps5).

Safety argument is unchanged from v1.8.1: zip-bomb defence is
check_space() (statvfs-based real disk-space check before staging) +
max_ratio (declared compression ratio cap). The size caps are a UX guard,
not a security boundary.

RAR extraction inherits the new defaults automatically (rar_extract.c
threads c->limits from the engine).

84 host-side checks (70 ZIP + 14 RAR), 0 failures. PS5 cross-compile
succeeds; ELF size grew from 427 656 (v1.8) to 509 704 (v1.8.2) due to
the dmc_unrar vendor TU being linked in.
2026-09-05 16:10:31 +08:00
Songlx516 bf55a4e4fd relax(extract): default limits 64 GiB -> 256 GiB / 512 GiB -> 1 TiB / 200 -> 500 (v1.8.1 hotfix)
User feedback: the previous default limits were an over-cautious UX
guard, not a security guard. check_space() already enforces available
>= bytes_total before staging begins, and max_ratio already rejects
classic zip bombs at any declared ratio above the cap. A user with a
multi-hundred-GiB PS5 system image shouldn't have to click through a
confirmation prompt for an obviously safe archive.

k_default_limits (src/zip_extract.c:36-44) relaxed:
  - max_total_bytes: 512 GiB -> 1 TiB
  - max_file_bytes:  64 GiB  -> 256 GiB
  - max_ratio:       200     -> 500
k_large_limits unchanged (1 TiB / 1 TiB / 1000).

Frontend threshold LARGE_FILE_THRESHOLD_BYTES (assets/main.js:838)
bumped 60 GiB -> 240 GiB to track the new default. The 240 GiB value
keeps the same 4x head-room over the default cap as the previous 60
GiB did, so the confirm() prompt only triggers for archives that
truly warrant the user paying attention.

assets/lang-{en,zh}.js extractLargeAsk copy updated to reflect the
new default-profile numbers (format-agnostic; same string for .zip and
.rar).

README.md: 'Stricter default' line under "What's new in v1.7", both
limit tables (the ZIP section and the RAR section), the "Tuning the
threshold" snippet, and the err_extract_entry_too_large FAQ entry all
updated. CHANGELOG.md gets a new [v1.8.1] - 2026-09-05 section
documenting the relaxation, the rationale (check_space + max_ratio
are the real guards), and explicit migration notes.

docs/HANDOVER.md and docs/UPGRADE-v1.8-rar-support.md numeric
references updated. The historical v1.7 upgrade document
(docs/UPGRADE-v1.7-zip-large-file-profile.md) is deliberately left
unchanged so the v1.7 -> v1.8.1 evolution remains traceable from git.

src/rar_extract.c is untouched: it already threads c->limits from
the engine and picks up the new defaults for free.

tests/test_zip_extract.c test_large_profile() rewritten for the new
ratio cap:
  - medium_bomb.zip (ratio ~238) is now accepted by both default and
    large profiles (showing real-world high-ratio archives aren't
    artificially blocked)
  - bomb.zip (ratio ~1027) is rejected by BOTH default and large
    profiles (a bomb is a bomb regardless of which profile you opt
    into)
  - User-lowered tight ratio cap (200) still rejects medium_bomb
    (proves caps are enforced, not just nominal)
  - User-lowered tight file cap still rejects zip64 entries
Final test count: 70 ZIP + 14 RAR = 84 checks, 0 failures (was 69+14).

Migration:
  - Forward-compatible: existing v1.7/v1.8 deployments that never
    triggered err_extract_entry_too_large see no difference
  - No data loss: relaxation only widens accepted archives
  - Real guards (check_space, max_ratio, path traversal) unchanged
2026-09-05 15:42:47 +08:00
Songlx516 848b774333 docs: v1.8 RAR support (CHANGELOG, README, UPGRADE, HANDOVER, NOTICE, i18n)
CHANGELOG.md — new [v1.8] — 2026-09-05 section covering Added / Changed
/ Limitations / Vendored / Verification / Technical notes / Credits.
ELF sha256 + size remain TBD until the user runs the WSL cross-compile;
the banner is anchored on the v1.7 sha256 for traceability.

README.md — adds 'What's new in v1.8' (immediately after the title
banner) describing RAR single-volume extraction. The new 'RAR
extraction' section documents scope (single-volume, RAR4 + RAR5,
unencrypted, plain file entries), limits (multi-volume/encrypted
rejected with surface-level err_extract_unsupported, symlinks/FIFOs
rejected, RAR1.4/1.5 rejected), error semantics, frontend wiring,
security notes, vendoring notes, and the v1.9 upgrade path.
'Project layout' tree gains third_party/unrar/ and the test runners
gain test_rar_extract.c. 'FAQ' err_extract_unsupported entry rewritten
to mention the RAR scope. Test count and ELF size estimate bumped to
83 checks and ~469 KiB respectively (dmc_unrar adds ~51 KiB).

docs/UPGRADE-v1.8-rar-support.md (new, 641 lines) — mirrors the
v1.7 upgrade document style. Includes architecture overview, facade-
header rationale, full rar_translate_error mapping table, frontend
detail, test coverage map, and a clear roadmap section listing what
v1.8 explicitly does NOT do and how v1.9 addresses each item.

docs/HANDOVER.md — §4 progress table updated (v1.7 -> done, v1.8 ->
substantially complete; WSL cross-compile + git push still pending
user-side). §9.1 corrects the license note: dmc_unrar is GPL-2.0-or-
later, NOT the 'UnRAR license' from the original plan. §12 snapshot
moved to v1.8; new §14 'v1.8 actual delivery state' written for the
hand-off recipient — captures what is actually in the tree vs. the
original §6 plan, and lists the remaining user-side steps verbatim.

THIRD_PARTY_NOTICES — section 3 added attributing DrMcCoy/dmc_unrar
1.7.0 (GPL-2.0-or-later), noting dmc_unrar_api.h is project-authored,
and pointing at third_party/unrar/COPYING for full license text.

assets/lang-{en,zh}.js — err_extract_unsupported rewritten to make
the RAR scope visible ('only unencrypted plain ZIP and single-volume
RAR are supported'). Both languages cover the same surface; the i18n
team can adjust tone later without changing meaning.
2026-09-05 15:18:34 +08:00
Songlx516 5af5acf4ab chore(test): regenerate byte-stable ZIP fixtures (date_time fixed to 1980)
Tests/make_fixtures.py now patches zipfile.time so Python 3.13's
zipfile.writestr no longer embeds wall-clock date_time into each entry
(the module uses time.localtime(time.time())[:6] when the caller
supplies a string arcname, which silently made every fixture diff on
each regeneration and polluted git with thousands of unrelated byte
changes). The patch fakes localtime/time to always return
(1980, 1, 1, 0, 0, 0).

All .zip fixtures are regenerated against the new deterministic
generator and committed. notar.rar is regenerated alongside because
test_rar_extract.c's main() recreates it from basic.zip at run time;
including it here keeps the source fixture and the rar fixture in
lock-step.

Verified: 2 consecutive python3 make_fixtures.py runs produce identical
md5 for all 19 .zip fixtures; full test suite stays green
(69 ZIP + 14 RAR = 83 checks, 0 failures).
2026-09-05 15:18:24 +08:00
Songlx516 96286cb9f2 test(rar): 14 negative-path checks + rar fixture builder with rar/7z/placeholder fallback
tests/test_rar_extract.c — 14 checks, zero dependency on host having
rar or 7z installed:

  test_engine_dispatch (4):
    - not-a-RAR rejected: notar.rar (renamed basic.zip) -> FORMAT
    - junk blob rejected: junk.rar (1024 random bytes) -> FORMAT
    - missing source -> OPEN
    - null rar_path / dst -> INTERNAL
  test_format_translation (6):
    - rar_translate_error mapping table covers every dmc_unrar_return
      code rar_extract can emit, verified against fixture files
  test_limits_handoff (4):
    - default vs large limits profile passed through unchanged
    - max_entries / max_total_bytes / max_file_bytes / max_ratio

Uses tests/posix_compat.h (open/close/mkdirat renames) via gcc -include
for host builds -- never compiled into the PS5 payload.

tests/make_fixtures.py — adds rar_fixtures() that prefers host 'rar'
(non-free) and falls back to host '7z' (LGPL). If neither is present,
emits an 11-byte 'placeholder' basic.rar so the negative tests still
fire (basic.rar is positive-path, but is only read when a RAR writer
exists). Wired into the fn list in main().

tests/run-tests.sh:
  - Builds third_party/unrar/dmc_unrar.o with -DDMC_UNRAR_DISABLE_BE32TOH_BE64TOH=1
  - Builds rar_extract.o with -Ithird_party/unrar and posix_compat shim
  - Builds test_rar_extract.o against both rar_extract.o and zip_extract.o
    (the latter is required: rar_extract.c references zipx_status_string,
    zipx_default_limits, zipx_limits_profile for error/log/output)
  - Links test-rar-extract, then runs both test-zip-extract and
    test-rar-extract in sequence.

Fixtures committed:
  - basic.rar    -- 11-byte placeholder (host has no RAR writer)
  - notar.rar    -- basic.zip renamed, for format-rejection test
  - junk.rar     -- 1024 random bytes, for format-rejection test
  - All other .rar fixtures (encrypted, multi-volume, symlink, ...) are
    generated on demand when run on a host with rar/7z installed.

Final test count on a writer-less host: 69 ZIP checks + 14 RAR checks
= 83 total, 0 failures (re-verified post-commit).
2026-09-05 15:17:17 +08:00
Songlx516 50eb1734c1 feat(extract): RAR4/RAR5 single-volume backend via dmc_unrar facade
src/rar_extract.{c,h} (1276 LOC) — new public engine:
  rar_extract(rar_path, dst_dir, conflict, limits, cancel, progress, userdata, result)
Mirrors the zip_extract contract exactly: same three-phase template
(scan -> extract to .wfm-extract-*.tmp staging + fsync -> publish via
rename -> cleanup/rollback), same zipx_status_t / zipx_limits_t /
zipx_conflict_t / zipx_result_t / zipx_progress_t types, same callback
signatures. Reuses publish_dir/publish_entry/publish_staging so a RAR
extract behaves identically to a ZIP extract under conflict-policy
(overwrite / merge / fail), rollback, fsync, and per-entry fsync.

Internals:
  - rarx_ctx_t carries limits, callback, staging dir, published paths,
    detail-msg accumulator, and the nameset for path-conflict detection.
  - rar_translate_error maps every dmc_unrar_return code to a
    zipx_status_t (UNSUPPORTED volumes/encryption/method -> UNSUPPORTED,
    unsupported_link -> SPECIAL, oversized entry -> LIMIT_FILE,
    CRC fail -> CRC, etc.) so the frontend needs zero per-engine branching.
  - normalize_name converts backslash -> forward slash (RAR names may use
    either on Windows) and detects directory entries by trailing slash
    rather than relying on external attributes alone.
  - open_parent_dirs / check_space / remove_tree / cleanup_staging /
    rollback_published reused from zip_extract's pattern.

src/extract.c — adds extract_dispatch(file_task_t*, conflict, limits,
result*) routing .zip -> zipx_extract(), .rar -> rar_extract(), else
ZIPX_ERR_UNSUPPORTED ('only .zip and .rar are accepted'). The existing
extract_worker now calls dispatch instead of zipx_extract directly.
ends_with_ci() helper for the .rar suffix match (case-insensitive, trims
trailing '/' from paths that the frontend might send).

Makefile:
  - VERSION_TAG := v1.8 (was v1.7)
  - COMMON_SRCS adds src/rar_extract.c
  - THIRD_PARTY_SRCS adds third_party/unrar/dmc_unrar.c (compiled as its
    own TU; never inlined into rar_extract.c)
  - THIRD_PARTY_CFLAGS adds -Ithird_party/unrar and
    -DDMC_UNRAR_DISABLE_BE32TOH_BE64TOH=1 to avoid <endian.h> conflicts
    with the SDK's headers.

Limitations surfaced as ZIPX_ERR_UNSUPPORTED with frontend-visible
err_extract_unsupported messages:
  - multi-volume RAR (.partNN.rar chains) -- dmc_unrar upstream
    limitation; documented v1.9 plan is to vendor opello/unrar C++
  - encrypted RAR (password= ignored) -- dmc_unrar upstream limitation
  - symlinks / FIFOs / device nodes -- ZIPX_ERR_SPECIAL
  - RAR1.4 / RAR1.5 -- ZIPX_ERR_UNSUPPORTED

No ELF rebuild attached in this commit (cross-compile is WSL-side and
deferred to the release commit).
2026-09-05 15:09:32 +08:00
Songlx516 0d24359d80 vendor: add DrMcCoy/dmc_unrar 1.7.0 (GPL-2.0, single-file UnRAR)
Vendored verbatim from upstream tag 'v1.7.0' (commit d0f3efe, 2024-09-14):
  - dmc_unrar.c (375310 bytes, upstream unmodified)
  - example.c (upstream reference, not used in build)
  - COPYING (GPL-2.0-or-later)
  - README.md (upstream README)

Project-authored additions:
  - dmc_unrar_api.h: facade header exposing only the symbols rar_extract.c
    uses (opaque archive struct + init/close/open/get_*_file/extract/*_to_path).
    Avoids inlining dmc_unrar.c into rar_extract.c, which would pollute the
    dmc_unrar_io_handler struct field names (open/close) with the host-test
    posix_compat.h macros (wfm_open/wfm_close).
  - VENDORED.md: rationale for choosing dmc_unrar over opello/unrar, supported
    formats (RAR4/RAR5, single-volume, unencrypted), explicit limitations
    (no volumes, no encryption, no symlinks, no RAR1.4/1.5), and the full
    v1.9 upgrade recipe (vendor opello/unrar C++, swap RAROpenArchiveEx API,
    rar_extract.c signature unchanged).

GPL-2.0 license obligation: web-file-mgr.elf must ship GPL notice + source
(THIRD_PARTY_NOTICES + this directory; both added in the next commit batch).
2026-09-05 15:09:03 +08:00
Songlx516 bfe522e56b Show extract button for RAR (single + multi-volume) archives
User reported FTP-upload-and-extract use case: after uploading
.part01.rar..part05.rar via FTP, the extract button should appear on
selection just like .zip already does, alongside the existing delete /
copy / move / rename buttons in the toolbar.

Changes are purely client-side. The extractBtn sits next to deleteBtn
in the toolbar (HTML layout unchanged). New helpers:

- isRarMainVolume(item): matches bare .rar AND .part01.rar/.part1.rar
  / .part001.rar, but excludes any .partNN.rar (those are sub-volumes)
- isRarSubVolume(item): matches .part02+.rar (the non-first parts)
- isExtractableArchive(item): .zip OR rar main volume

renderExtractButton rewiring:
- archives.length + subs.length both zero -> hidden (unchanged default)
- only subs selected -> button visible but disabled with tooltip
  'select the main volume (.rar or .part01.rar)' (new i18n keys)
- exactly one archive selected -> button enabled with hover target

actionExtract wired to the new isExtractableArchive filter, keeping
the existing 'must be exactly 1 selected' guard so multi-select still
no-ops cleanly.

Caught a real bug while wiring: the first version of isRarMainVolume
matched /\.rar$/ before the part01 check, so part02+ was incorrectly
classified as a main volume. Fixed by reordering + adding negative
sub-pattern so .partNN. never trips the bare-.rar rule.

Two new i18n keys (zh + en):
- extractSelectMainVolume: friendly message when user picks only
  sub-volumes
- extractArchivePending: reserved for the future RAR password prompt

10/10 unit cases pass for the detection regexes
(zip / .rar / .part01.rar / .part02.rar / .tar.gz / .bin).

Backend support for RAR is not part of this commit; clicking Extract
on a .rar on v1.7 still returns err_extract_unsupported from the
existing zip_extract pipeline. That will swap to the unrar engine in
v1.8 without any further frontend work.

Also sets local repo core.autocrlf=false to keep JS files LF on this
Windows dev machine (global autocrlf was rewriting them as CRLF on
write and causing the whole file to show up as changed on every edit).
2026-09-05 09:07:10 +08:00
Songlx516 c7d9e965b8 v1.8: drop legacy RAR .rNN multi-volume format
Per user decision on 2026-09-04 21:01 GMT+8, the v1.8 plan no longer
covers the legacy split-archive format (name.rar + .r00/.r01/.r02...).

Rationale: PS5 users almost exclusively ship the modern RAR5
part-volume format (name.part01.rar, name.part02.rar, ...); the
legacy .rNN format is from the 2005-2010 era. Removing it cuts:

- Frontend: isRarSubVolume() no longer needs /\.r\d{2,}$/ branch
- Fixtures: drop rar4_multi_old.rar generation
- Tests: drop test_rar4_multi_volume_old()
- Backend: unrar API surface still supports legacy .rNN, but we just
  never feed it one

Documentation sweep:
- Section 5.1: range updated
- Section 6.4.1: isExtractableArchive/isRarSubVolume simplified
- Section 6.5.1: fixture generation script updated
- Section 6.5.3: test_rar4_multi_volume_old removed
- Section 8.4: decision rationale no longer mentions .rNN
- Section 6.6.4: CHANGELOG / README example updated
- Section 11.2: test matrix updated
- Section 6.4.9: D4 manual test list no longer tests .r00
2026-09-04 21:03:34 +08:00
Songlx516 3f516b51ad Add HANDOVER.md — project work plan for v1.8 RAR support
Comprehensive handover document for a developer taking over the project.
Covers project background and key facts, v1.7 ZIP large-file profile across
every layer, current progress snapshot, v1.8 RAR (single + multi-volume +
encrypted) detailed 6-day plan with day-by-day tasks, validation checklists,
key code templates, engineering rationale, 10 known pitfalls, common
commands, test matrix.

Document size: 1529 lines, 48 subsections. Designed to be the single
source of truth for continuing v1.8 development without context from
the original conversation.
2026-09-04 20:58:56 +08:00
Songlx516 aef4a44ab3 Add v1.7 changelog and technical upgrade notes
CHANGELOG.md: Keep-a-Changelog format, single v1.7 entry covering the
ZIP large-file profile, three-phase engine, host test suite and known
limitations; verification snippet.

docs/UPGRADE-v1.7-zip-large-file-profile.md: long-form technical write-up
for maintainers — architecture delta, engine/task/HTTP contract, frontend
wiring, tests, build pipeline, compatibility matrix, trade-offs and
post-v1.7 roadmap.
2026-09-04 14:27:52 +08:00
Songlx516 5cb0b76e7a Initial import of PS5 Web File Manager v1.7
Homebrew HTTP file manager payload for jailbroken PS5 consoles (x86_64-sie-ps5,
title id FMGR88888). Browse, edit, upload, download and extract ZIPs through
any browser on the LAN; single self-contained ELF, no external services.

Highlights (v1.7):
- ZIP large-file profile (large=1): 2 TiB total / 1 TiB per entry / 1000:1
  ratio. Frontend prompts for confirmation whenever archive > 60 GiB on disk.
- Default ZIP profile stays safe: 512 GiB / 64 GiB / 200:1.
- 69 host-side C tests (tests/run-tests.sh) covering traversal, ZIP64,
  encryption rejection, conflict policies and the large-file profile.
- Three-phase extraction engine (scan -> extract -> publish -> cleanup) with
  atomic staging + rename, located in src/zip_extract.{c,h}.

Project layout:
  src/          C payload sources
  assets/       HTML / CSS / JS / icons / param.json
  tests/        POSIX/host test suite + fixtures
  third_party/  vendored zlib + minizip-ng (zlib license)
  docs/screenshots/  README screenshot images
  LICENSE       GPLv3+

Build:
  make         # cross-compile with PS5_PAYLOAD_SDK
  make linux   # Linux host binary for UI dev
2026-09-04 14:21:15 +08:00