Found during the FW 13.60 hardware sweep: - Non-ASCII names inside a .zip were garbled on upload (名前 é → σÉìσëì ├⌐): zips from macOS/Linux store UTF-8 without the UTF-8 flag and the zip crate decodes those as CP437. Use the raw bytes when they are valid UTF-8 (as Info-ZIP/7-Zip do), for uploads and Convert. Verified on both consoles: 3001/3001 files byte- and name-identical. - A backup snapshot that copied nothing (an empty save) still left a 0-file entry in the backup list; the directory is now removed. - "Reopen where you left off" never worked since the landing moved from What's New to Home: the landing redirect beat the restore effect. And the leftover /whats-new trigger hijacked opening What's New by its address (this is what failed the bundled-docs e2e). The landing redirect now reads the saved route (lib/lastRoute, tested). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Engine
The Rust workspace: everything between the desktop app and the PS5.
The engine is a local HTTP service. The Tauri app doesn't talk to the
console directly — it calls the engine, which speaks FTX2 to the payload.
That split is deliberate: the same API is reachable from a browser, a
script, or CI, so nothing the app can do is locked inside the app. See
the self-hosted-engine entries in ../FAQ.md.
client (Tauri/React) ──HTTP──▶ ps5upload-engine :19113
│ FTX2 binary framing
▼
payload :9113 transfer
:9114 management
Crates
| Crate | What it is |
|---|---|
ftx2-proto |
Wire format: frame type IDs, header layout, constants. Shared by everything that speaks FTX2. |
ps5upload-core |
The bulk of the logic — connection handling and socket tuning, transfer/resume/verification, filesystem and app RPCs, volume parsing, package install, saves, cheats, hardware, SMB, BPS patching. |
ps5upload-engine |
The Axum HTTP service (~100 routes), job tracking, SSE progress events, and the desktop + mobile entry points. |
ps5upload-pkg |
.pkg parsing — headers, entries, split-file sets. |
ps5upload-lab |
CLI for driving the payload's control channel by hand. Useful when you want one frame, not a workflow. |
ps5upload-tests |
Integration tests against an in-process mock FTX2 server. |
ps5upload-bench |
Throughput benchmarks. |
Working on it
cargo test --workspace # no PS5 required — mock server
cargo build --release -p ps5upload-engine
Run it standalone and point it at a console:
PS5_ADDR=<ip>:9113 cargo run -p ps5upload-engine
curl "http://127.0.0.1:19113/api/ps5/status?addr=<ip>:9114"
PS5UPLOAD_ALLOW_IP lets another machine reach it. The API is
unauthenticated and can read, write and delete files on the console —
keep it on a trusted LAN.
Testing
engine/crates/ps5upload-tests/tests/ runs against a loopback mock
server: single-file, streaming, directory, packed small-file shards,
resume-after-drop, retry classification, digest mismatch, exclude rules,
.zip/.7z streaming, hardware commands, and volume parsing. Unit tests
live beside their modules.
Anything touching real transfer behaviour still needs hardware — see
../TESTING.md.
Things that will bite you
- The payload emits JSON; serde parses it. Field names must be
snake_caseor serde silently leaves the field at its default — a zero or an empty string, not an error. One malformed byte rejects the whole response, so anything interpolated into a payload-side JSON string has to be escaped first. ok: falseinside a 200 is a refusal, not a success. Several payload commands answer that way.client/src/api/ps5.tshas anassertOk()for action endpoints; status endpoints keepok:falseas data because there it means "unsupported on this console".- Two ports, two roles. Transfer frames go to 9113, everything else to
9114. The wrong port returns
wrong_portrather than working oddly.