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
PS5 Web File Manager
Homebrew HTTP file manager for jailbroken PS5 consoles. Browse, edit, upload, download and extract ZIPs through any browser on the same network — single self-contained ELF payload, no external services, no telemetry.
Version: v1.9.2 · Title ID: FMGR88888 · License: GPLv3+ · Target: x86_64-sie-ps5
Overview
A payload ELF that runs an HTTP file manager inside a jailbroken PS5. Open http://<PS5_IP>:8888/ from any browser on the LAN — including the PS5 browser itself — to manage files on attached USB storage and the user partition. Designed for safely copying game-dump folders from USB to internal storage, but it also handles general file management, in-place text editing, PKG preview/install, image preview, and ZIP extraction with built-in zip-bomb protection.
The same source tree builds a Linux binary for development and a PS5 payload ELF for deployment — see make linux below.
What's new (unreleased)
-
Encrypted ZIP extraction works end to end — both schemes:
- traditional PKWARE ("ZipCrypto", what
zip -ewrites), and - WinZip AES-128/192/256 (compression method
99plus the0x9901extra field, what7z -mem=AES256and WinZip write), for stored and deflated entries.
A missing or wrong password is reported as
extract_password— the code the task overlay already knew how to translate, even though until now nothing could produce it for a ZIP. The SHA-1, HMAC-SHA1 and AES primitives live in a new local minizip-ng backend,third_party/minizip-ng/src/mz_crypt_wfm.c(PBKDF2 comes from the vendoredmz_crypt.c); its header explains why they are implemented in-tree instead of delegating to another vendored library. - traditional PKWARE ("ZipCrypto", what
-
Encrypted RAR is actually wired up. v1.9 vendored an engine that could decrypt (
RARSetPassword) but never called it, so encrypted archives were rejected. The password now reaches the engine, including for-hpheader-encrypted archives, and a wrong password returnsextract_passwordso the prompt can retry. -
Build fix: compiler-flag changes now invalidate objects.
makecannot see flag changes, so adding-DHAVE_WZAES -DHAVE_PKCRYPTleft the existingmz_zip.o/mz_crypt.oin place — and because nothing referenced the new streams any more,--gc-sectionsquietly dropped the encryption code again while the link still "succeeded". The Makefile now records the third-party flag set inps5-obj/.third_party_cflagsand rebuilds only when it really changes. This is the same trap theLzmaDec.orule was working around. -
The UI can now retry with a password. An
extract_passwordfailure no longer ends in an error box: the attempt is re-sent with whatever the user types, up to three times, keeping the conflict policy and the large-file opt-in of the original request. Cancelling or submitting an empty box falls back to the original failure report. 7z keeps its up-front prompt, since an encrypted 7z header would otherwise cost a wasted scan. -
Encrypted 7z headers (
-mhe=on) now open. This was the last known format gap: with-mhe=onthe file names, the folder table and every entry size sit inside the encrypted header, so the vendored SDK gives up withSZ_ERROR_UNSUPPORTEDbefore it can list a single entry. A new module,src/sevenz_header.c, reads the header record, decodes its one folder with the project's own 7zAES path (src/sevenz_chain.c) and then hands the SDK a small virtual stream in which the encrypted record has been replaced by the plaintext — so the SDK goes on parsing exactly the archive it always did, and nothing on disk is touched. Archives whose header is merely compressed (-mhc=on, the default) are not modified in any way, and a wrong password comes back asextract_passwordlike every other encrypted archive. -
tests/make-zip-enc-fixtures.batand three real fixtures undertests/fixtures-real/(enc-zipcrypto.zip,enc-aes256.zip,enc-aes256-store.zip, passwordsecret123). -
Host checks: 140 ZIP + 37 RAR = 177 (
tests/run-tests.sh). The new encrypted-ZIP cases run against real archives intests/fixtures-real/generated by the newtests/make-zip-enc-fixtures.bat. -
A RAR archive that asks for a dictionary larger than this build supports no longer reports as a per-entry size problem: it gets its own
extract_dict_too_largecode and a message that names the required and the supported dictionary size. The build's behaviour is unchanged — such an archive is still refused, because honouring it would mean allocating the whole window in one shot, which is exactly what rarlab's own CLI refuses to do by default and what a 16 GB shared-memory console cannot afford. -
7z checks: 27 cases, 0 failures (
tests/run-sevenz-tests.sh), and theKNOWN_GAPSlist that heldaesheis now empty — the encrypted-header fixture passes through both the folder decoder and the extraction facade. -
The frontend retry flow has a headless check of its own —
node .build/ui_retry_test.mjsloads the realassets/main.jsinto a stubbed DOM and asserts the remembered request, the retry cap and the give-up paths, including the regression case for a non-ASCII folder: 40 checks, 0 failures.node .build/ui_upload_menu_test.mjscovers the markup side — everydata-i18nkey exists in both languages, the upload menu is wired to the right handlers, the classes it uses are styled, and the row-highlight rules are scoped so they cannot lose the cascade to a generic button rule, and that the extract button is never hidden — only disabled, with a message per reason: 40 checks, 0 failures. -
The tree builds to 903 448 B, sha256
8ca47d5aaca75085b32641300cce30fadb7df7749cb6b53d04f129bcecc286b7,e_machine0x003e. Rebuilt from the same tree, byte-identical both times; the built ELF was checked to contain the new frontend code, which is only reachable after gunzipping the embedded assets. Same 20 sections and no new dynamic symbols. The encrypted-archive work added +5 712 bytes of content (.text+4 880,.rodata+640,.eh_frame*+192); the fork marker below then added a further +0x100 (256 B), the correctederr_extract_unsupportedcopy another +0x40 (64 B), the upload menu +0x980 (2 432 B), the frontend copy and CSS of the first fix round +0x100 (256 B) and the scoped row-highlight rules +0x180 (384 B), then the always-on extract button +0x240 (576 B) — every one of them to.rodataand to no other section, so the file is still 903 448 B across all six builds. An unchanged size is not evidence of an unchanged binary — compare sections withreadelf -SW. -
The version string carries a fork marker:
v1.9.3M. Upstream releases are plainvX.Y.Z, so the trailingM(Modified) is what tells you which of the two projects a build came from. It is part ofVERSION_TAG, so/api/version, the PS5 start-up notification, the stdout banner, the UI footer and the ELF file name all carry it at once, and the UI footer spells it out on hover. The file name changing also means a fork build can no longer shadow an upstream artifact of the same upstream version. See Credits. -
Still open: end-to-end validation of the built ELF on a real console.
This section describes unreleased work: the published release is still
v1.9.2and its binary does not contain any of it. The unreleased tree identifies itself asv1.9.3M.
What's new in v1.9.2
Version-string-only re-release. The v1.9.1 tag sat four commits behind the
tree that produced its binary, so the tag could not rebuild the published
artifact; v1.9.2 is cut from the right commit. It is functionally identical to
the v1.9.1 binary — the only change is the baked-in version string.
What's new in v1.9
- RAR engine replaced with the official rarlab UnRAR 7.20.1
(
third_party/unrar7/, replacing dmc_unrar). This is what actually makes RAR extraction work on real files: dmc_unrar could not decode archives written by WinRAR 6.x/7.x (RAR5 "v6" compression) and had no multi-volume support — both now work. - RAR5 "v6" archives extract (the v1.8-era "corrupt archive" report on WinRAR 6/7 files is gone).
- Multi-volume RAR (
.part01.rarchains): unrar stitches the parts by name when the full set sits next to the volume you open. - Engine can decrypt encrypted RAR (
RARSetPassword) — password UI / API plumbing still pending, encrypted archives are rejected for now. - Host tests now run real archives (v6 / encrypted / 3-volume fixtures
committed under
tests/fixtures-real/): 70 ZIP + 24 RAR = 94 checks.
What's new in v1.8
- Single-volume RAR extraction via the vendored FLOSS library
dmc_unrar(GPL-2.0-or-later). RAR 1.5, 2.x, 3.x, 4.x and 5.x archives are supported..rarfiles appear in the file list with the Extract button enabled; the button is greyed out on.part02+.rarsub-volumes with the tooltip "select the main volume instead" — v1.8 cannot stitch multi-volume RARs (see the RAR extraction section below). - Shared extraction protocol between the new
src/rar_extract.cengine and the existingsrc/zip_extract.cengine: samezipx_status_tcodes, samezipx_limits_tprofile (default /large=1), same three-phase model (scan → extract → publish → cleanup), same staging directory layout, same conflict policy, same error mapping into the task UI. The dispatcher insrc/extract.cis one tinyends_with_ci(…)switch. - 14 new host-side C tests (
tests/test_rar_extract.c) wired into the existingtests/run-tests.sh. Coverage: format dispatch, error translation across everyDMC_UNRAR_*code that affects RAR users, limit-profile handoff. Total host checks: 69 ZIP + 14 RAR = 83. - Documentation:
CHANGELOG.md,docs/UPGRADE-v1.8-rar-support.mdand the vendoring decision tree atthird_party/unrar7/VENDORED.md(v1.8 shipped it asthird_party/unrar/VENDORED.md). - See the dedicated section below for scope and the limitations that come from using dmc_unrar (no multi-volume, no encryption in v1.8 — both lift in v1.9 when the library is replaced).
What's new in v1.8.1
- Default ZIP limits relaxed (companion to v1.7's large profile).
v1.7 shipped with a 64 GiB default per-entry cap, which was too
aggressive for typical PS5 system-backup ZIPs (200-300 GiB). v1.8.1
raises the default profile to 1 TiB total / 256 GiB per entry /
500 : 1 ratio, with the
large=1opt-in kept at 2 TiB / 1 TiB / 1000 : 1. The frontend threshold rises from 60 GiB to 240 GiB so common system-backup archives no longer trigger the prompt. - RAR extraction inherits the new defaults (rar_extract.c threads
c->limitsfrom the engine — no engine change required). - Rationale: the real 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.
What's new in v1.8.2
- Default ZIP limits relaxed again for the 3A-game single-file case.
A single ~300 GiB uncompressed file inside an archive was still
silently rejected by v1.8.1 (the default scan returns
ZIPX_ERR_LIMIT_FILE_SIZEbefore the request ever reaches the frontend confirmation prompt). v1.8.2 raises the default profile to 2 TiB total / 512 GiB per entry / 500 : 1 ratio, with thelarge=1opt-in bumped to 4 TiB / 1 TiB / 1000 : 1. Frontend threshold rises from 240 GiB to 480 GiB. - Two PS5-only build fixes discovered when cross-compiling for the
PS5 target. The host-side test suite (
tests/run-tests.sh) had silently accepted both because it links the same sources but uses gcc rather than clang 18 and a different include path:MakefileCFLAGS: add-Ithird_party/unrarsosrc/rar_extract.ccan find the project-authoreddmc_unrar_api.hfacade header.src/extract.c: moveextract_progress()definition aboveextract_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).
- Release artifact for v1.8.2:
web-file-mgr.elf— 509 704 bytes, sha2561b2c3d68b35e32737105f17d14a80a3c159ceca0cabd274ee168cbcd81906f65, ELF class 64, little-endian, e_machine0x003e(x86_64-sie-ps5). - Tests: 84 host-side checks (70 ZIP + 14 RAR), 0 failures. PS5 cross-compile succeeds end-to-end.
What's new in v1.7
- ZIP large-file profile (opt-in via the new
large=1argument on/api/extract): relaxed caps of 2 TiB archive total, 1 TiB per entry, 1000 : 1 compression ratio. The frontend prompts for confirmation whenever the archive on disk is larger than 60 GiB; the server only activates the profile when the user explicitly agrees. - Stricter default ZIP profile stays safe: 1 TiB total / 256 GiB per entry / 500 : 1 ratio. A 4 MiB compressed payload that expands to 800 GiB still gets rejected before any output file is opened.
- 69 host-side C tests (
tests/run-tests.sh) now cover path traversal, ZIP64, encryption rejection, ratios, conflict policies and the new large-file profile (tests/test_zip_extract.c). - Earlier refinements — see
git logsince v1.6.
Screenshots
Features
- Browse — list files and folders; sort by name, type, size, mtime or permissions. Last sort mode persists in
localStorage. - Permissions — toggle read/write/execute with checkboxes, or paste a validated four-digit octal mode.
- Operations — copy, move, delete (recursive, no recycle bin), rename, create files and folders.
- Editor — in-place UTF-8 text editor for files ≤ 1 MiB across a curated extension list:
.txt .json .xml .ini .cfg .conf .md .log .lua .js .css .html .htm .c .h .cpp .hpp .sh .csv .yaml .yml .shn. - Multi-select — copy, move, delete or tar-download many items in one go.
- Upload — single files or folder trees from any device on the LAN (hidden in the PS5 browser). Atomic temp + rename.
- Download — single file as raw bytes, or folders/multi-select as a streaming
.tar. Hidden in the PS5 browser. - Tasks — full-screen overlay with delayed show, live progress, throughput, ETA, cancel, and recovery if the browser is closed and reopened mid-task.
- Archive extraction — ZIP, RAR and 7z, all behind the same zip-bomb / traversal / ratio protection. ZIP covers stored / deflated / ZIP64 plus encrypted entries (ZipCrypto and WinZip AES-128/192/256); RAR covers RAR4 + RAR5 including WinRAR 6/7 "v6", multi-volume, and
-p/-hpencryption; 7z covers Copy / LZMA / LZMA2 / PPMd, the Delta and BCJ2 filters,.7z.001volumes, 7zAES, and-mhe=onencrypted headers. See the ZIP extraction and RAR extraction sections below for scope. - Encrypted archives — a wrong or missing password is reported as
err_extract_passwordand retried through a password prompt, up to three times, with the original conflict policy and large-file opt-in preserved. 7z asks for the password up front instead, so an encrypted header does not cost a wasted scan. - PKG — install and preview
.pkgfiles. - Images — preview
.png .jpg .jpeg .gif .bmp .webp. - Localization — English + Simplified Chinese, auto-selected from
navigator.languages. - Mobile-friendly — responsive layout with wrapped toolbars and horizontally scrollable file lists.
Quickstart
-
Build the ELF:
export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk # see "Build" for SDK setup make -
Send the payload to the PS5 (default ELF-loader port
9021):nc -q0 "$PS5_HOST" 9021 < web-file-mgr.elf -
Read the on-screen PS5 notification — it prints the actual listen port (default
8888). -
Open
http://<PS5_IP>:<port>/in any browser on the same LAN — the PS5 browser works too. -
On first run, the payload also writes a Media-category home-screen launcher; existing launcher files are not overwritten.
Build
Requires ps5-payload-dev/sdk:
export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
This project links against libmicrohttpd. make checks for it before building and runs the installer automatically when missing:
make
If the build host has no network access, drop the libmicrohttpd tarball in advance and run the installer manually:
LIBMICROHTTPD_TARBALL=/path/to/libmicrohttpd-1.0.1.tar.gz \
./install-libmicrohttpd.sh
make
Output:
web-file-mgr.elf (~several hundred KiB, larger in v1.9 with unrar; x86_64-sie-ps5)
For pure UI/JS work without the PS5 toolchain:
make linux
./web-file-mgr-linux
The Linux build does not include the PS5 home-screen launcher installer.
Usage
Start an ELF loader on the PS5 (port 9021 is common). Send the payload:
export PS5_HOST=ps5_ip_address
nc -q0 "$PS5_HOST" 9021 < web-file-mgr.elf
After the payload starts, the PS5 notification shows the app name, version and actual listen port. Open the URL it prints, for example:
http://${PS5_IP_ADDRESS}:8888/
If the payload had to fall back to a different port (e.g. 8889), use whatever port the notification shows — the URL is not hard-coded.
On first startup, the payload installs a PS5 Web File Manager shortcut in the Media category when needed. Existing launcher files are preserved; only missing ones are written.
ZIP extraction
Plain and encrypted ZIPs — stored / deflated / ZIP64, in the clear or with either encryption scheme (traditional PKWARE "ZipCrypto" and WinZip AES-128/192/256). The engine is a standalone three-phase module (scan → extract → publish → cleanup) at src/zip_extract.{c,h}, with a separate host-side C test suite. Each entry is first written into a staging directory (*.wfm-part-*), then atomically renamed into the destination. There is deliberately no per-entry fsync anywhere in the extract path — the whole pipeline is "sync nothing, rename everything", because publish is rename-only and there is no resume feature to protect (measured ≥14x on an 8000-file archive; see docs/EXTRACTION-PERF.md). Any failure mid-archive rolls back partial changes; cancel and fatal errors always clean up staging.
Limits
| Limit | Default profile | Large profile (ZIPX_LIMITS_LARGE) |
|---|---|---|
max_entries |
200 000 | 500 000 |
max_total_bytes (uncompressed) |
2 TiB | 4 TiB |
max_file_bytes (per entry) |
512 GiB | 1 TiB |
max_ratio (uncompressed / compressed) |
500 : 1 | 1000 : 1 |
max_depth (folder nesting) |
32 | 32 |
max_name_len / max_path_len |
255 / 1024 | 255 / 1024 |
The default profile is shipped safe: a 4 MiB compressed blob that decodes to 800 GiB is rejected before any output file is opened. The large profile is engaged only when the request includes large=1 — the archive dialog prompts the user automatically whenever the archive on disk is larger than LARGE_FILE_THRESHOLD_BYTES (480 GiB by default; configurable in assets/main.js). Confirming the prompt is the user's explicit opt-in; the server still records nothing extra on its own.
Security checks
The engine refuses to extract:
- Path traversal (
..segments, absolute POSIX paths, Windows drive letters). - Symbolic links, devices, FIFOs, sockets (
ZIPX_ERR_SPECIAL). - Duplicate entries or directory/file name clashes inside the same archive.
- Archives whose expanded size, entry count, depth, name length or compression ratio breach the active profile.
Encrypted entries are no longer a refusal: the password arrives as password=
on /api/extract, and a missing or wrong one comes back as ZIPX_ERR_PASSWORD
(err_extract_password in the UI) so the prompt can retry. The scan phase still
runs for encrypted archives — handing over a password does not skip the limits.
Conflict policy
Passed as conflict= on /api/extract:
fail(default) — refuse to overwrite any existing target.overwrite— replace existing files; merge into existing folders.merge— keep existing files, add new ones.
Tuning the threshold
The 480 GiB frontend threshold lives in assets/main.js:
const LARGE_FILE_THRESHOLD_BYTES = 480 * 1024 * 1024 * 1024;
Set it to Infinity to silence the prompt, lower it to be more conservative, or remove the call entirely — the server still respects large=1 regardless of the threshold.
RAR extraction
A RAR extraction engine (src/rar_extract.{c,h}) backed by the official
rarlab UnRAR source (third_party/unrar7/, version 7.20.1, compiled as a
static library and driven through its C-compatible DLL API). Files with the
extension .rar get the same Extract button as .zip files; the engine
is dispatched by src/extract.c based on extension.
v1.9 replaced the v1.8 engine (dmc_unrar 1.7.0). dmc_unrar could not decode archives written by WinRAR 6.x/7.x (RAR5 "v6" compression) and had no multi-volume support; unrar handles both natively.
Scope
| Format | Support | Notes |
|---|---|---|
| RAR 1.5 → 4.x (incl. 2.9 / 3.6 / 4.0) | ✅ | |
| RAR 5.0 and 5.0 "v6" (WinRAR 6.x / 7.x) | ✅ | The v1.9 trigger |
| Solid blocks, dictionary up to 1 GiB | ✅ | |
| PPMd decompression (RAR 3.0+) | ✅ | |
Multi-volume (.part01.rar + .part02.rar + …) |
✅ | unrar stitches parts by name when the whole set sits next to the volume you open. Select the first volume (name.part1.rar / name.part01.rar); non-first volumes are still greyed out in the UI with a hint. |
| Encrypted RAR | ✅ | Both -p data encryption and -hp header encryption. The password reaches the engine as password= on /api/extract (RARSetPassword runs after RAROpenArchiveEx and before the first RARReadHeaderEx); a missing or wrong one returns ZIPX_ERR_PASSWORD so the prompt can retry. |
| Symbolic links / FIFOs / sockets / devices | ❌ | Rejected with ZIPX_ERR_SPECIAL (mirrors ZIP behaviour) |
| RAR 1.3 (pre-1.4) | ❌ | Rejected upstream by unrar |
When an archive is rejected, the user gets an extract_unsupported
failure with the file name as the detail argument. The frontend already
shows this with the typical bilingual retry guidance.
Limits
The RAR engine re-uses the ZIP limits table verbatim — there is no RAR
profile table on top. Defaults and the large=1 opt-in are identical:
| Limit | Default profile | Large profile (large=1) |
|---|---|---|
max_entries |
200 000 | 500 000 |
max_total_bytes (uncompressed) |
2 TiB | 4 TiB |
max_file_bytes (per entry) |
512 GiB | 1 TiB |
max_ratio (uncompressed / compressed) |
500 : 1 | 1000 : 1 |
max_depth (folder nesting) |
32 | 32 |
max_name_len / max_path_len |
255 / 1024 | 255 / 1024 |
Large-profile RAR extraction uses the same LARGE_FILE_THRESHOLD_BYTES
(480 GiB) prompt as ZIP — the frontend treats .rar and .zip the same
way for the prompt, and the server only ever activates the large caps
when the request carries large=1 (opt-in).
Security checks
The RAR engine applies the same checks as the ZIP engine — re-uses
zipx_status_t codes, so the task UI's err_extract_unsafe_name,
err_extract_too_deep, err_extract_ratio, etc. all fire identically:
- Path traversal (
..segments, absolute POSIX paths, Windows drive letters,\treated as a path separator after aRar!\x1a\x07…header, etc.). - Symbolic links, FIFOs, sockets, devices.
- Duplicate entries or directory/file name clashes inside the archive.
- Archive size, entry count, depth, name length or compression ratio breaches of the active profile.
Vendoring and licence
third_party/unrar7/ is a verbatim copy of the official rarlab UnRAR
source (7.20.1), mirrored by
opello/unrar at commit 97e1780. It is
distributed under the UnRAR freeware licence (see
third_party/unrar7/license.txt): it may be used in any software to handle
RAR archives, but may not be used to develop a RAR-compatible archiver or
re-create the RAR compression algorithm. The project-authored facade
third_party/unrar7/unrar_c_api.h carries the project's own licence.
The v1.8 engine
third_party/unrar/dmc_unrar.c(DrMcCoy/dmc_unrar 1.7.0, GPL-2.0-or-later) was removed in v1.9; its notice lives in git history.
Encrypted RAR
The password channel is complete: password= on /api/extract is handed to
the engine, and RARSetPassword runs after RAROpenArchiveEx and before the
first RARReadHeaderEx — the order unrar needs to decrypt a -hp header. A
missing or wrong password comes back as ZIPX_ERR_PASSWORD
(err_extract_password in the UI), which is what raises the password prompt
and re-sends the original request, up to three times.
7z extraction
The 7z engine (src/sevenz_extract.{c,h}) is built on the LZMA SDK decode
subset plus the project's own pull-based codec chain
(src/sevenz_chain.c). Files ending in .7z get the same Extract button as
.zip and .rar; src/extract.c dispatches by extension, and the engine
re-uses the same three-phase model, limit profiles and conflict policy.
Added in v1.9.1. The SDK's own
SzArExpath only understands folders with up to four coders, which cannot express BCJ2's five — hence the self-parsed folder table and the pull-based chain.
A note on the header. 7-Zip keeps the archive header at the end of the file and
compresses it when it grows (-mhc=on, the default), which is why the header
region normally starts with an k7zIdEncodedHeader record describing one
folder. With -mhe=on that folder is also encrypted, and since it holds the
file names, the folder table and every entry size, the vendored SDK gives up on
the whole archive before listing anything. src/sevenz_header.c handles that
case: it reads the record, decodes its folder through the same 7zAES path the
content uses, and then presents the SDK with a virtual stream whose header
region is the plaintext — the archive on disk is never written to, and an
archive whose header is merely compressed is not touched at all.
Scope
| Format | Support | Notes |
|---|---|---|
| Copy / LZMA / LZMA2 (incl. ZIP64-style sizes) | ✅ | A single-coder pure-LZMA2 folder decodes multi-threaded (src/sevenz_mt.c, 8 threads) |
| BCJ2 (x86 branch converter) | ✅ | Through the self-written chain; not expressible in the SDK's SzArEx |
| Multi-coder folders, Delta filter, PPC / IA64 / ARM / ARMT / SPARC converters | ✅ | Parsed by src/sevenz_chain.c |
Volumes (.7z.001 / .z01 chains) |
✅ | src/sevenz_volstream.c stitches by name; open the first volume |
| Content encryption (7zAES, AES-256-CBC) | ✅ | The engine decrypts; the frontend asks for the password up front (so an unencrypted archive does not pay a wasted scan), passes it as password=, and retries on ZIPX_ERR_PASSWORD |
-mhe=on (encrypted header) |
✅ | src/sevenz_header.c decodes the header record itself (through the same 7zAES path) and hands the SDK a virtual stream carrying the plaintext; a wrong password reports ZIPX_ERR_PASSWORD, so the prompt retries like any other encrypted archive |
-mhc=off (uncompressed header) |
✅ | Plain headers were always readable; they are now read one byte at a time and left alone |
Limits
The 7z engine re-uses the ZIP limits table verbatim — see ZIP extraction → Limits.
Security checks
The same zipx_status_t codes and the same checks as ZIP and RAR: path
traversal, special files, duplicate/clashing entries, and size, entry-count,
depth, name-length or ratio breaches of the active profile.
Verification
After make, sanity-check the produced ELF:
ls -la web-file-mgr.elf # size grew in v1.9 (unrar static library); ~509 KiB was v1.8.3
sha256sum web-file-mgr.elf # record the digest in your release notes
file web-file-mgr.elf # expect "ELF 64-bit LSB pie executable, x86-64"
od -An -tx1 -N20 web-file-mgr.elf | head -2 # magic 7f45 4c46 0201 + e_machine 003e
The e_machine = 0x003e confirms the PS5 target triple x86_64-sie-ps5. The e_type = 3 (ET_DYN) confirms the position-independent payload expected by ELF loaders.
Tests
A POSIX/host-side C test suite covers the ZIP, RAR and 7z engines and runs on any Linux / macOS / MSYS shell without the PS5 SDK:
cd tests && bash run-tests.sh # ZIP + RAR suites
bash run-sevenz-tests.sh # 7z suite (needs MinGW gcc + a 7-Zip binary)
Output is a per-case check-style report — 177 checks on the current main
(140 ZIP + 37 RAR), 0 failures. Coverage:
- ZIP entry parsing (stored + deflated + ZIP64)
- Path traversal, absolute paths, backslash, Windows drive letters
- Symbolic links, FIFOs, bad CRC, truncated archives, non-ZIP files
- Limits:
entries,total_bytes,file_bytes,ratio,depth,name_len - Conflict policies:
fail/overwrite/merge - Cancellation in every phase
- Encrypted archives — each real fixture is run four ways (no password,
empty password and wrong password →
ZIPX_ERR_PASSWORD; correct password → success with a byte-level content check):enc-zipcrypto.zip,enc-aes256.zipandenc-aes256-store.zipon the ZIP side,enc-v6.raron the RAR side. Two further cases prove the limits still apply once a password has been handed over, and that the failing paths publish nothing. - Large-file profile —
medium_bomb.zip(ratio ≈ 238) is rejected under default caps and accepted under large caps; lowered large caps still enforce. - RAR engine (
tests/test_rar_extract.c, 37 checks) — format dispatch (renamed ZIP rejected, junk blob rejected), error translation across every reachable engine code, limits handoff (thelarge=1opt-in flows intorar_extract()unchanged), the oversized dictionary path above, plus the real-archive coverage above. - 7z engine (
tests/test_sevenz_extract.c+tests/run-sevenz-tests.sh) — byte-for-byte comparison against real.7zfixtures, encrypted-header rejection, conflicts under every policy, cancellation, limits, a missing destination parent, and the guarantee that a failure publishes nothing and cleans up its staging tree.
The frontend retry flow has its own headless check —
node .build/ui_retry_test.mjs loads the real assets/main.js into a stubbed
DOM and asserts the remembered request, the retry cap and the give-up paths:
27 checks, 0 failures. It lives in .build/ (outside the gitignore
whitelist), so it is a development-time script rather than a committed test.
Project layout
.
├── Makefile # PS5 + Linux builds (VERSION_TAG v1.9.3M)
├── install-libmicrohttpd.sh # one-shot dependency installer
├── gen-asset-module.py # embeds assets/* as gzip-compressed C arrays
├── assets/ # HTML / CSS / JS / icons / param.json
├── src/ # C payload sources
│ ├── main.c websrv.c filemgr.c # entry, HTTP frontend, task model
│ ├── upload.c download.c text.c # stream and in-place edit handlers
│ ├── extract.c # /api/extract dispatcher (ZIP + RAR + 7z)
│ ├── zip_extract.{c,h} zipx_common.c # ZIP engine (minizip-ng backend)
│ ├── zipx_volume.c zipx_volstream.c # ZIP volume detection + concatenating stream
│ ├── rar_extract.{c,h} # RAR engine (rarlab UnRAR 7.20.1 backend)
│ ├── sevenz_extract.{c,h} sevenz_chain.{c,h} # 7z engine, self-parsed codec chain
│ ├── sevenz_header.{c,h} # 7z header reader / -mhe=on decryption
│ ├── sevenz_mt.c sevenz_volstream.c # multi-threaded LZMA2 + .7z.001 volumes
│ ├── app_installer.c pkg_installer.c pkg_info.c # PS5 PKG preview / install
│ └── demangle_stub.c cpu_support_stub.c # size / portability stubs
├── third_party/ # vendored libraries
│ ├── unrar7/ # rarlab UnRAR 7.20.1 — RAR engine
│ ├── minizip-ng/ # 4.2.2, trimmed to the read path
│ ├── 7z/ # LZMA SDK 26.03 decode subset
│ └── zlib/ # minizip's compression backend
├── tests/ # POSIX/host test suite
│ ├── test_zip_extract.c test_rar_extract.c test_sevenz_extract.c
│ ├── sevenz_chain_e2e.c sevenz_e2e.c bigfile_e2e.c
│ ├── make_fixtures.py make_sevenz_fixtures.py make_split_fixtures.py
│ ├── run-tests.sh # one-shot runner (ZIP + RAR suites)
│ ├── run-sevenz-tests.sh # 7z suite, carries the KNOWN_GAPS list
│ ├── bench_driver.py bench_formats.py # throughput benchmarks
│ ├── compat/ # tiny Win32/MSYS shims
│ └── fixtures/ fixtures-7z/ fixtures-real/
├── docs/
│ ├── HANDOVER.md # v1.8-era playbook, historical — see the root HANDOVER.md
│ ├── SIZE-OPTIMIZATION.md # ELF size analysis + per-symbol ledger
│ ├── EXTRACTION-PERF.md # decompression benchmarks
│ ├── REWRITE-FEASIBILITY.md # engine-extraction study
│ ├── UPSTREAM-V1.8-COMPARISON.md
│ ├── UPGRADE-v1.7-zip-large-file-profile.md
│ ├── UPGRADE-v1.8-rar-support.md
│ └── screenshots/ # README screenshot images
├── THIRD_PARTY_NOTICES # per-library licence summary
├── HANDOVER.md # current engineering handover
├── LICENSE # GPLv3+
└── README.md
Notes
- Copy, move, delete, upload and download run as single background tasks. While one task is running, other file operations are rejected.
- Delete is recursive and permanent. There is no recycle bin.
- Copy/move tasks can be canceled. A partially copied single file is removed, but partially copied folders are left in place to avoid deleting pre-existing files when merging into an existing target folder.
- Upload tasks can be canceled. A partially uploaded temporary file is removed when possible.
- Downloading a folder or multiple selected items produces a tar stream. The tar archive is generated by the payload and is not written to PS5 storage first.
- The UI can recover the active task display if the browser is closed and reopened while the payload process is still running.
- Text editing is limited to the curated extension list above. Non-UTF-8 and oversized files are rejected.
- File names are transmitted as UTF-8 through the web API. The payload also preserves legacy byte-oriented names returned by mounted filesystems so mixed USB filename encodings still display and operate correctly.
FAQ
- This is a homebrew app and should not intentionally modify system processes or kernel memory. If you hit a kernel panic, make sure you are using a recent jailbreak method and ELF loader, or revert to the stable method you normally use.
- P2JB users — if this payload triggers a kernel panic, avoid using it on that setup. Stability matters more than convenience when each retry is expensive.
- The preparing stage can take a while when a folder contains many files — it sums folder size and checks free space, which helps avoid starting a copy / move / upload / download that cannot finish safely.
err_extract_entry_too_large— default archive caps are 512 GiB per entry / 500:1 ratio (covers a typical 3A-game archive with one ~300 GiB uncompressed file). If you exceed the default, confirm the large-file prompt (appears for archives > 480 GiB on disk), split the archive, or passlarge=1directly to the API.err_extract_unsupported— the archive is one this build cannot read: a file that is neither.zipnor.rarnor.7z, a ZIP entry using a compression method other than stored/deflated, a 7z folder with an unsupported coder, a split set whose naming is not recognised (a RAR set namedx.rar.001must be renamed tox.part1.rar,x.part2.rar, …), or a RAR older than 1.4. Encrypted and multi-volume archives are not in this category — both are supported. The backend's own sentence is appended in parentheses and names the actual cause.err_extract_dict_too_large— a RAR archive declares a compression dictionary larger than this build supports (4096 MiB) and unrar asked for permission to exceed it. The message states both the size the archive needs and the size the build allows. This is refused on purpose: the alternative is a single allocation of the entire dictionary window, which rarlab's own CLI rejects by default and which a 16 GB shared-memory console cannot sustain. Recompress the file on a PC with a dictionary of 4 GiB or less (-md), or extract it there. Note that the RAR5 format itself caps the field at 4 GiB, so this can only come from an archive written in the newer RAR7 header format.err_extract_password— the archive is encrypted and the password was missing or wrong. That includes a 7z archive with an encrypted header (-mhe=on): the file names and entry sizes live inside the header, so nothing at all can be listed until the header decrypts. ZIP and RAR raise a password prompt on failure and retry the same request with what you type (up to three times; cancel or an empty box gives up); 7z asks before it starts, since an encrypted 7z header would otherwise cost a wasted scan.
Credits
This project is a fork of owendswang/ps5-web-file-manager (GPL-3.0). The web UI, the task model and the PS5 packaging all originate there, and the upstream author's release under GPL-3.0 is what makes this derivative work possible.
Telling a fork build from an upstream one: since v1.9.3 the version string
carries an M suffix (vX.Y.ZM) — M for Modified. Upstream owendswang
releases are plain vX.Y.Z. So v1.9.2 is upstream/fork-shared numbering while
v1.9.3M can only have come from this repository; the same letter appears in
the ELF file name, the PS5 start-up notification, /api/version and the web UI
footer. Releases before v1.9.3M predate the convention and keep their plain
numbers.
Built with reference to these projects:
- ps5-payload-dev/websrv: HTTP server structure, static asset embedding ideas, PS5 browser/websrv behaviour and PKG install function. License: GPLv3+.
- ps5-payload-dev/ftpsrv: PS5 payload conventions, home-screen launcher/install flow reference, process handling style and startup installation reference. License: GPLv3+.
- seregonwar/zftpd: PS5 TCP socket buffer tuning and high-throughput transfer behaviour reference. License: MIT.
- itsPLK/ps5-payload-manager: Payload building behaviour. License: GPLv3.
- libmicrohttpd: Used as the embedded HTTP server library. Licensed by GNU under the LGPL; this payload links it as the SDK-provided static library.
- ps5-payload-dev/sdk: Payload building foundation. License: GPLv3+.
- etaHEN: ShellUI URI navigation used to return to the PS5 home screen before exit. License: GPLv3.
- ezremote: cited for the PKG-preview feature. License: GPL-2.0-only — its source files carry no "or later" notice, so it is not combinable with this GPL-3.0 codebase. No code was taken from it:
src/pkg_info.cis an independent C99 implementation (it also reads the.pkgentry table andparam.jsonfields, for which ezremote has no counterpart, and it uses the hand-written tokenizer insrc/json_util.crather than json-c). Seedocs/REWRITE-FEASIBILITY.md§2.2. - zlib-ng/minizip-ng: ZIP reader used by the
/api/extractendpoint. Vendored underthird_party/minizip-ng/. License: zlib. - zlib: Compression backend for minizip-ng. Vendored under
third_party/zlib/. License: zlib. - rarlab UnRAR — RAR reader used by the
/api/extractendpoint since v1.9. Vendored underthird_party/unrar7/(version 7.20.1, the RARDLL source set). License: UnRAR freeware license — seethird_party/unrar7/license.txt. Note this is a restricted licence rather than a FLOSS one: it permits using the source to handle RAR archives but forbids using it to build a RAR-compatible compressor. - opello/unrar — the mirror the vendored rarlab sources were fetched from (commit
97e1780). - DrMcCoy/dmc_unrar — RAR engine shipped in v1.8 only, superseded in v1.9 by rarlab UnRAR (it could not decode RAR5 "v6" archives or multi-volume sets). Removed from the tree; its licence was GPL-2.0-or-later.
License
The project is distributed under GPLv3 or later, matching the GPLv3+ projects used as implementation references. See LICENSE.
Third-party projects retain their own licenses. Do not copy assets or source from the credited projects into another distribution without preserving the corresponding license notices.
If distributing binaries, comply with the LGPL terms for libmicrohttpd
in addition to this project's GPL license. The vendored zlib and
minizip-ng sources are distributed under the zlib license; retain the
copyright notices in third_party/zlib/LICENSE and
third_party/minizip-ng/LICENSE when redistributing binaries built
with this feature.
The vendored third_party/unrar7/ sources (rarlab UnRAR — the RAR engine
behind src/rar_extract.c) are not GPL: they ship under the UnRAR
freeware license (see third_party/unrar7/license.txt), which forbids
using them to develop a RAR-compatible compressor. Keep that notice and
that restriction intact when redistributing. THIRD_PARTY_NOTICES carries
the full per-library summary.
Disclaimer
Unofficial homebrew software. Runs only on jailbroken PS5 consoles. Use at your own risk — the authors are not responsible for damage, data loss, account action or warranty impact. Do not redistribute Sony-proprietary content. Under GPLv3+, modified redistributions must publish their sources.





