24 KiB
Changelog
All notable changes to PS5 Web File Manager are documented in this file. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Release artifact for v1.9:
web-file-mgr.elf— size 919 440 bytes (~897 KiB) sha256bb8f17e9addc6a9984f611503ca01b51f8984b773d353630da1f25bd1a28a997ELF class 64, little-endian, e_machine0x003e(x86_64-sie-ps5)Source delta vs v1.8.3: RAR engine replaced (dmc_unrar 1.7.0 → rarlab UnRAR 7.20.1,
third_party/unrar/→third_party/unrar7/), newsrc/rar_extract.cscan/extract implementation, Makefile + host-test C++ rules, 5 real RAR fixtures committed. See [v1.9] below.Release artifact for v1.8.3:
web-file-mgr.elf— size 509 704 bytes (~497 KiB) sha256fdcf7b09b69e2160e77dfa084c0e890ba0696d4dd478b1d5ff499cdc9f527955ELF class 64, little-endian, e_machine0x003e(x86_64-sie-ps5)Source delta vs v1.8.2: 5 files touched (4 user-facing + 1 build pipeline) — see [v1.8.3] below for details.
Release artifact for v1.8.2:
web-file-mgr.elf— size 509 704 bytes (~497 KiB) sha2561b2c3d68b35e32737105f17d14a80a3c159ceca0cabd274ee168cbcd81906f65ELF class 64, little-endian, e_machine0x003e(x86_64-sie-ps5)Source delta vs v1.8.1: 5 files relaxed (
src/zip_extract.ck_default_limits
- k_large_limits,
assets/main.jsLARGE_FILE_THRESHOLD_BYTES,assets/lang-{en,zh}.jscopy,tests/test_zip_extract.cadvertised-number assertion, README/HANDOVER numeric references) + 2 PS5-only build fixes (MakefileCFLAGS-Ithird_party/unrar,src/extract.cforward declaration ofextract_progress); no vendored or engine changes.
[v1.9] — 2026-09-05
RAR engine replaced: rarlab UnRAR 7.20.1 (v6 / multi-volume / decryption-capable).
The vendored dmc_unrar 1.7.0 only dispatched RAR5 compression version 5
(switch(file->version) case 0x5000). Archives written by WinRAR 6.x /
7.x (algorithm string v6, version field 0x5001) hit the default branch
and surfaced as "corrupt archive" — confirmed on a real v6:8M archive.
v1.9 swaps in the official rarlab UnRAR source (7.20.1) via its
C-compatible DLL API, compiled as a static library (-DRARDLL, PS5 uses
the toolchain's default libc++).
What this enables:
- RAR5 "v6" compression (WinRAR 6/7 archives) — the v1.9 trigger.
- Multi-volume RAR (
.partNN.rar): unrar stitches volumes by name when all parts sit next to the opened volume. Select the first volume (name.part1.rar) and extract as usual. - RAR4 and older RAR5 remain supported.
- The engine can decrypt encrypted archives (
RARSetPassword), but the password channel (API + UI) is not wired yet — encrypted headers/entries still fail up front witherr_extract_unsupported. Planned for a follow-up.
Engine changes:
src/rar_extract.crewritten to unrar's sequential DLL API (RAROpenArchiveEx → RARReadHeaderEx → RARProcessFile); scan and extract each re-open the archive. Multi-volume continuation segments (RHDF_SPLITBEFORE) are advanced but not re-counted/deduped.- The three-phase scan → staging → publish/rollback machinery is unchanged.
- Bug fix:
normalize_name()no longer clears the caller's directory flag (a real v6 archive with an explicit directory header after its files tripped the duplicate detector).
Build & test:
- Makefile:
.cpprules for the unrar RARDLL source set (49 files, mirrorsUnRARDll.vcxproj); links through the C++ driver;VERSION_TAGv1.9. - tests: 5 real RAR fixtures committed under
tests/fixtures-real/(generated withtests/make-rar-fixtures.bat+ WinRAR); new happy-path checks extract a real v6 archive, verify files on disk, auto-merge a 3-volume split, and reject encrypted archives. Total: 70 ZIP + 24 RAR = 94 checks (up from 70 + 14; the old 14 RAR checks never ran a real archive).
Credits: unrar (c) Alexander Roshal, freeware license — see
third_party/unrar7/license.txt and THIRD_PARTY_NOTICES.
[v1.8.3] — 2026-09-05
Hotfix: "Upload and extract" now accepts .rar files.
The "upload and extract" entry was hard-coded to accept only .zip,
even though the server-side dispatch in src/extract.c:79 already
correctly routes .rar to rar_extract(). v1.8.3 fixes the frontend
filter so users can select a single-volume plaintext .rar from the
file picker and have it uploaded + extracted in one click (the same
flow that already worked for .zip).
What this enables:
- Choose a single-volume
.rarfrom "Upload and extract" - Server extracts it via the existing
rar_extract()engine - Uploaded
.raris auto-deleted after a successful extract (same as.zipsince v1.7)
What this does not enable (planned for v1.9.0):
- Multi-volume RAR (e.g.
name.part01.rar+name.part02.rar…) - Encrypted RAR (password-protected headers or entries)
Both still return extract_unsupported "single-volume RAR only" /
"encrypted RAR is not supported; please extract on a PC first" — see
the underlying engine limit in third_party/unrar/dmc_unrar (GPL-2.0,
1.7.0). v1.9 will swap the vendor to opello/unrar 7.20.1 (UnRAR
License) which natively supports both.
Changed:
assets/index.html—<input id="uploadZip" accept>now lists.rar+ the two RAR MIME types next to the existing ZIP entries.assets/main.js:2340—/\.zip$/i→/\.(zip|rar)$/i(the upload pre-check), plus a localisRarflag so the next step branches.assets/lang-en.js—extractUploadConfirm: "uploaded ZIP" → "uploaded archive".assets/lang-zh.js—extractUploadConfirm&extractLargeAskdrop the "ZIP" wording so the copy reads sensibly for RAR uploads.assets/main.js:38—APP_VERSION"v1.7"→"v1.8.3"(footer version string had been hard-coded to v1.7 since the frontend was first imported; it no longer misleads about which build is running).
No backend changes — the server side was already correct. No test
changes — the existing RAR happy-path test in tests/test_rar_extract.c
passes against the same backend.
Build pipeline (also v1.8.3):
.build/build-elf.shstep 6 "no source change → skip make" check now also watchesassets/*andgen-asset-module.py, not justsrc/*.cand theMakefile. Without this, v1.8.3 (which touched no backend, only frontend assets feedinggen/*.c) was misclassified as "no change" andmakewas skipped — the result was that the v1.8.2 ELF was reported as v1.8.3 with the same sha256. With this fix, only frontend changes correctly trigger a rebuild. Users running the WSL build need to re-copy.build/build-elf.shto/home/song/build-elf.sh(canonical source is on the Windows side).
[v1.8.2] — 2026-09-05
Hotfix: default ZIP extraction limits cover 3A-game single-file archives.
The default k_default_limits profile is now 2 TiB total / 512 GiB per
entry / 500 : 1 ratio (was 1 TiB / 256 GiB / 500 : 1 in v1.8.1). The
frontend threshold LARGE_FILE_THRESHOLD_BYTES is bumped from 240 GiB to
480 GiB to match. The large=1 profile is widened to 4 TiB total
/ 1 TiB per entry / 1000 : 1 ratio (was 2 TiB / 1 TiB / 1000 : 1); the
large profile must always be strictly more permissive than default.
Why: a 3A-game archive with a single ~300 GiB uncompressed file was
silently rejected by the default profile (scan_archive returns
ZIPX_ERR_LIMIT_FILE_SIZE in src/zip_extract.c line ~717 — the request
never reaches the frontend confirmation prompt, so the user just sees
"卡壳"). The 256 GiB default cap was tuned for PS5 system backups (which
have many smaller entries, not a single huge file) and was wrong for the
3A-game single-file case. The default cap is now 512 GiB so a typical
3A archive extracts under the default profile without prompting.
The 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 through from the engine, so no rar-side change is
required.
Changed
src/zip_extract.c—k_default_limitsrelaxed:max_total_bytes: 1 TiB → 2 TiBmax_file_bytes: 256 GiB → 512 GiBmax_ratio: 500 → 500 (unchanged)
src/zip_extract.c—k_large_limitswidened (must stay > default):max_total_bytes: 2 TiB → 4 TiBmax_file_bytes: 1 TiB → 1 TiB (unchanged)max_ratio: 1000 → 1000 (unchanged)
assets/main.js—LARGE_FILE_THRESHOLD_BYTES: 240 GiB → 480 GiBassets/lang-{en,zh}.js—extractLargeAskcopy updated to reflect the new numbers (default 512 GiB / 2 TiB; large 1 TiB / 4 TiB)tests/test_zip_extract.c—test_large_profileadvertised-number assertion updated:max_total_bytes == 4 TiB(was 2 TiB)README.md— both limit tables (ZIP + RAR), the "Tuning the threshold" snippet, and theerr_extract_entry_too_largeFAQ entry bumped to the new numbersdocs/HANDOVER.md—LARGE_FILE_THRESHOLD_BYTES, thek_default_limits/k_large_limitsASCII diagram, the user-scenario description, the 480 GiB popup note, and the RAR limits paragraph bumped to the new numbers
Unchanged
src/rar_extract.c— already threadsc->limitsfrom the engine, picks up the new defaults for freemax_ratio— both profiles unchanged (500 : 1 default / 1000 : 1 large)check_space()— unchanged; still the real disk-space guardmax_entries— 200 000 default / 500 000 large, unchangeddocs/UPGRADE-v1.7-zip-large-file-profile.md— historical v1.7 document left as-is so the v1.7 → v1.8.2 evolution is traceable- Test fixture
medium_bomb.zip(ratio ≈ 238) still exercises both rejection under the default 500 : 1 cap and acceptance under thelarge=11000 : 1 cap - 84 host-side checks (70 ZIP + 14 RAR), 0 failures
Migration notes
- Forward-compatible — users with v1.8.1 deployments who never trigger
err_extract_entry_too_largesee no difference (defaults are strictly more permissive) - 3A-game single-file archives now extract silently — no prompt, no
manual
large=1API call required for files up to 512 GiB - No data loss — the relaxation only widens accepted archives; the
real security guards (
check_space,max_ratio,path traversal) are untouched - No frontend UX change for typical use — only archives > 480 GiB on disk now trigger the confirmation prompt (previously 240 GiB)
[v1.8.1] — 2026-09-05
Hotfix: relaxed default ZIP extraction limits.
The default k_default_limits profile is now 1 TiB total / 256 GiB per
entry / 500 : 1 ratio (was 512 GiB / 64 GiB / 200 : 1). The frontend
threshold LARGE_FILE_THRESHOLD_BYTES is bumped from 60 GiB to 240 GiB
to match. The large=1 profile (1 TiB / 1 TiB / 1000 : 1) is unchanged.
Why: the previous default was a UX-oriented early-fail guard, not a
security guard — check_space() already enforces available ≥ bytes_total
before staging begins, and max_ratio already rejects classic zip
bombs. A user with a multi-hundred-GiB system image shouldn't have to
click through a confirmation prompt for what's a perfectly safe archive.
The relaxed default still rejects any archive whose declared
uncompressed total exceeds the destination's free space (real check,
not a declared-vs-fs assertion) and any archive with a declared ratio
above 500 : 1 (real zip-bomb guard).
RAR extraction inherits the new defaults automatically — rar_extract.c
threads c->limits through from the engine, so no rar-side change is
required.
Changed
src/zip_extract.c—k_default_limitsrelaxed:max_total_bytes: 512 GiB → 1 TiBmax_file_bytes: 64 GiB → 256 GiBmax_ratio: 200 → 500
assets/main.js—LARGE_FILE_THRESHOLD_BYTES: 60 GiB → 240 GiBassets/lang-{en,zh}.js—extractLargeAskdefault-profile copy updated to reflect the new numbersREADME.md— "Stricter default ZIP profile" line, the limit table (two locations), and theerr_extract_entry_too_largeFAQ entry bumped to the new numbers; "Tuning the threshold" snippet updated to 240 GiBdocs/HANDOVER.mdanddocs/UPGRADE-v1.8-rar-support.md— the few remaining numeric references in those docs updated
Unchanged
src/rar_extract.c— already threadsc->limitsfrom the engine, picks up the new defaults for freek_large_limits—large=1profile (1 TiB / 1 TiB / 1000 : 1) is unchangeddocs/UPGRADE-v1.7-zip-large-file-profile.md— historical v1.7 document left as-is so the v1.7 → v1.8.1 evolution is traceable- Test fixture
medium_bomb.zip(ratio ≈ 238) still exercises both rejection under the default 500 : 1 cap and acceptance under thelarge=11000 : 1 cap - 83 host-side checks (69 ZIP + 14 RAR), 0 failures
Migration notes
- Forward-compatible — users with v1.7 / v1.8 deployments who never
trigger
err_extract_entry_too_largesee no difference (defaults are strictly more permissive) - No data loss — the relaxation only widens accepted archives; the
real security guards (
check_space,max_ratio,path traversal) are untouched - No frontend UX change for typical use — only archives > 240 GiB on disk now trigger the confirmation prompt (previously 60 GiB)
[v1.8] — 2026-09-05
RAR extraction: single-volume RAR4 / RAR5 (unencrypted).
⚠️ Scope clarification — v1.8 ships single-volume unencrypted RAR only. The original RAR wishlist (multi-volume
.partNN.rar, encrypted RAR with password UI) is deferred to v1.9; seedocs/UPGRADE-v1.8-rar-support.mdandthird_party/unrar/VENDORED.mdfor the rationale and the engine upgrade path. ZIP behaviour and the large-file profile are unchanged.
Added
src/rar_extract.{c,h}— a new extraction engine that mirrorszip_extract's protocol exactly. Internally it wraps the vendoreddmc_unrar1.7.0 (third_party/unrar/dmc_unrar.c). The public entry point israr_extract(rar_path, dst_dir, conflict, limits, cancel, progress, userdata, result)— same signatures, samezipx_status_tcodes, samezipx_result_t, samezipx_limits_tprofile lookup. ~1276 LOC (src/rar_extract.c).third_party/unrar/:dmc_unrar.c— vendored verbatim from upstream (11 598 LOC, ~365 KiB). GPL-2.0-or-later, attributed inTHIRD_PARTY_NOTICES.dmc_unrar_api.h— project-authored facade header. Re-declares only thedmc_unrar_*symbolsrar_extract.cactually uses, so the engine can#include "dmc_unrar_api.h"instead of#include "dmc_unrar.c". This keeps the vendored.ccompiling as its own translation unit and avoids polluting dmc_unrar's struct / function names with any build-system macros (seetests/posix_compat.h'swfm_open/wfm_closerename pattern — that conflict is what motivated the facade). The facade carries the project's license; the library body remains unmodified.COPYING,README.md— upstream GPL notice + readme.VENDORED.md— explains why we chosedmc_unrarover rarlab UnRAR /opello/unrar, what is and is not supported, and gives a step-by-step upgrade plan for moving to a fuller C++ UnRAR in v1.9.
src/extract.cdispatch layer —extract_dispatch()picks the engine by extension (.zip→zipx_extract,.rar→rar_extract, anything else →ZIPX_ERR_UNSUPPORTED). The case-insensitive suffix matcher trims trailing path separators.extract_workernow calls the dispatch instead of going straight to the ZIP engine.- Frontend + i18n wiring:
assets/main.jsrecognises.rar(single-volume) and.part0*1.rar(multi-volume master) as extractable, greys out the extract button on.part02+.rarsub-volumes with a tooltip "select the main volume instead". This UX is only useful because v1.8 still rejects multi-volume RAR with a friendly error — the visual feedback stops the user from selecting a sub-volume and getting confused.assets/lang-{en,zh}.jserr_extract_unsupportedupdated to: "…(only unencrypted plain ZIP and single-volume RAR are supported)…".
- Host test suite (
tests/test_rar_extract.c, 14 checks) — negative paths only (format dispatch, error translation, limits handoff). The suite is wired intotests/run-tests.shalongside the existing ZIP suite; fixture generation falls back to a placeholder blob when norar/7zwriter is present, so the negative tests fire on any host. Total host checks: 69 ZIP + 14 RAR = 83.
Changed
Makefile:VERSION_TAG := v1.8(wasv1.7).THIRD_PARTY_SRCSaddsthird_party/unrar/dmc_unrar.c.THIRD_PARTY_CFLAGSadds-Ithird_party/unrarand-DDMC_UNRAR_DISABLE_BE32TOH_BE64TOH=1(dmc_unrar's own byte-swap helpers are avoided so we don't need an extrabyteswap.hshim on the SDK).
src/extract.c/src/extract.h— no public-API break. The HTTP surface (POST /api/extract) accepts the same fields as v1.7 plus the existinglarge=1; there is nopassword=field because v1.8 cannot decrypt. (The wire format is forward-compatible — a v1.9password=field will be additive.)THIRD_PARTY_NOTICES— adds a section3. dmc_unrarcrediting Sven Hesse (DrMcCoy), summarising the GPL-2.0-or-later obligations on the resulting binary, and noting thatdmc_unrar_api.his project-authored and licensed with the project.
Limitations (v1.8 scope)
- Multi-volume RAR (
.part02+.rar,.part1+.rar, numbered continuations) is rejected withZIPX_ERR_UNSUPPORTEDand the error message "extract on a PC first". Upstream dmc_unrar does not chain companion volumes by design. When opello/unrar replaces dmc_unrar in v1.9 this becomes a one-line error-code drop. - Encrypted RAR (any encrypted header / file flag) is rejected with
ZIPX_ERR_UNSUPPORTED. Same root cause — dmc_unrar omits decryption to avoid patent complications. There is no password prompt in the UI; there is nopassword=field in/api/extract. - Symbolic links, FIFOs, sockets, devices inside a RAR archive are
rejected with
ZIPX_ERR_SPECIAL(mirrors ZIP behaviour). - RAR 1.4 (very old) is not supported by dmc_unrar and is rejected
upstream with
DMC_UNRAR_ARCHIVE_VERSION_UNSUPPORTED; the wrapper maps that toZIPX_ERR_UNSUPPORTED. RAR 1.5 through RAR 5.0 are supported.
Verification
make # builds web-file-mgr.elf (in WSL)
ls -la web-file-mgr.elf # record size
sha256sum web-file-mgr.elf # record digest (paste into the v1.8 banner above)
file web-file-mgr.elf # ELF 64-bit LSB pie, x86-64
od -An -tx1 -N20 web-file-mgr.elf # 7f45 4c46 0201 + e_machine 003e
(cd tests && bash run-tests.sh) # 69 + 14 = 83 checks, 0 failures
python3 .build/check-elf-gzip.py ./web-file-mgr.elf # 7/7 v1.7 keys + 1 v1.8 key (err_extract_unsupported)
Technical notes
A long-form technical write-up of this upgrade lives in
docs/UPGRADE-v1.8-rar-support.md.
The vendoring decision tree (and the v1.9 plan) is in
third_party/unrar/VENDORED.md.
Credits
Same as v1.7 — see README.md → Credits. dmc_unrar
is credited in THIRD_PARTY_NOTICES.
[v1.7] — 2026-09-04
ZIP extraction: large-file profile, three-phase engine, host test suite.
Added
- Large-file profile (
ZIPX_LIMITS_LARGE) for ZIP extraction — relaxed caps of 500 000 entries, 2 TiB total uncompressed, 1 TiB per entry, 1000 : 1 compression ratio. Enable via the newlarge=1argument onPOST /api/extract. The UI prompts the user automatically whenever the archive on disk is larger than 60 GiB (LARGE_FILE_THRESHOLD_BYTES). - Standalone three-phase ZIP engine (
src/zip_extract.{c,h}) — the modelSCAN → EXTRACT → PUBLISH → CLEANUP. Each entry is written into a staging directory first, fsynced, then atomically renamed into the destination. Any mid-archive failure rolls back partial changes. - Profile lookup helper
zipx_limits_profile(int)and the new macrosZIPX_LIMITS_DEFAULT(0) andZIPX_LIMITS_LARGE(1). The publiczipx_extract()signature is unchanged. - Opt-in API field
largeonPOST /api/extract, backed by a newextract_largeflag infile_task_t(src/filemgr_internal.h,src/extract.c). - Frontend wiring (
assets/main.js) —LARGE_FILE_THRESHOLD_BYTES,shouldPromptLargeMode(),promptLargeMode(), consumed byactionExtract()anduploadAndExtractFile(). - Bilingual UI strings (
assets/lang-{en,zh}.js) —extractLargeAskandextractLargeActive. - Host-side C test suite (
tests/) — POSIX-runnable, no PS5 SDK required. 69 checks at release: traversal, ZIP64, encryption rejection, conflict policies, large-file profile switching. - Cross-compile helper (
.build/build-elf.sh) — staging-mode build that works around the read-only SDK install location. - Demo page (
.build/extract-demo.html) — visualises the three policy combinations (fail / overwrite / merge) against the new profile table.
Changed
- Engine internals rewritten around the three-phase model. Public API
(
zipx_extract(),zipx_default_limits()) is unchanged — old callers compile and link clean. file_task_tgainsextract_large(src/filemgr_internal.h). Internal-only; downstream consumers reading the task struct need to recognise the new field.gen-asset-module.pycontinues to gzip JS into the binary; the helpers in.build/check-elf-gzip.pyare the supported way to verify that a fresh build picked up asset changes (regularstringswon't see them).
Security
- Default ZIP caps are unchanged: 512 GiB total / 64 GiB per entry / 200 : 1 ratio.
- The large profile is never activated server-side on its own — the
client must explicitly send
large=1(either via the UI prompt or directly via the API). - Encryption, path traversal, symbolic links, FIFOs and unresolved conflicts are still rejected before any output file is opened.
- Strict value matching: only the literal
"1"enables the large profile;true,yes,onare all treated as0. Matches the existingremove_source=semantics.
Known limitations
- Multi-volume / split ZIP archives (
.zip+.z01,.z02, …) are not stitched by the engine. minizip-ng has the API; wiring it is post-v1.7. - The 60 GiB frontend threshold for the large-profile prompt is hardcoded
(
LARGE_FILE_THRESHOLD_BYTES,assets/main.jsline 813). - The large profile does not run
statvfs()againstdst_dirbefore extraction; free-space preflight is on the post-v1.7 roadmap. - Bomb-shaped archives with compression ratio > 1000 : 1 are rejected
under both profiles (
bomb.zipwith 4 MiB of'A'has ratio ≈ 1026 and hits this wall under the large profile).
Verification
make # builds web-file-mgr.elf
ls -la web-file-mgr.elf
sha256sum web-file-mgr.elf # 648e4a00…
file web-file-mgr.elf # ELF 64-bit LSB pie, x86-64
od -An -tx1 -N20 web-file-mgr.elf # 7f45 4c46 0201 + e_machine 003e
(cd tests && bash run-tests.sh) # 69 checks, 0 failures
python3 .build/check-elf-gzip.py ./web-file-mgr.elf # 7/7 large-mode keys in ELF
Technical notes
A long-form technical write-up of this upgrade (architecture delta, three-phase
model, behaviour contract, trade-offs, future work) lives in
docs/UPGRADE-v1.7-zip-large-file-profile.md.
Credits
The same as v1.6 — see README.md → Credits.