diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cdd77b..1679cba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,98 @@ All notable changes to **PS5 Web File Manager** are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -> Release artifact for v1.8.1: -> `web-file-mgr.elf` — size TBD (cross-compile runs in WSL — see `docs/HANDOVER.md`) -> sha256 TBD +> Release artifact for v1.8.2: +> `web-file-mgr.elf` — size 509 704 bytes (~497 KiB) +> sha256 `1b2c3d68b35e32737105f17d14a80a3c159ceca0cabd274ee168cbcd81906f65` > ELF class 64, little-endian, e_machine `0x003e` (x86_64-sie-ps5) > -> Source delta vs v1.8: 3 source files relaxed (`src/zip_extract.c` k_default_limits, -> `assets/main.js` `LARGE_FILE_THRESHOLD_BYTES`, `assets/lang-{en,zh}.js` copy); -> no vendored or engine changes. +> Source delta vs v1.8.1: 5 files relaxed (`src/zip_extract.c` k_default_limits +> + k_large_limits, `assets/main.js` `LARGE_FILE_THRESHOLD_BYTES`, +> `assets/lang-{en,zh}.js` copy, `tests/test_zip_extract.c` advertised-number +> assertion, README/HANDOVER numeric references) + 2 PS5-only build fixes +> (`Makefile` CFLAGS `-Ithird_party/unrar`, `src/extract.c` forward +> declaration of `extract_progress`); no vendored or engine changes. + +## [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_limits` relaxed: + - `max_total_bytes`: 1 TiB → **2 TiB** + - `max_file_bytes`: 256 GiB → **512 GiB** + - `max_ratio`: 500 → **500** (unchanged) +- `src/zip_extract.c` — `k_large_limits` widened (must stay > default): + - `max_total_bytes`: 2 TiB → **4 TiB** + - `max_file_bytes`: 1 TiB → **1 TiB** (unchanged) + - `max_ratio`: 1000 → **1000** (unchanged) +- `assets/main.js` — `LARGE_FILE_THRESHOLD_BYTES`: 240 GiB → **480 GiB** +- `assets/lang-{en,zh}.js` — `extractLargeAsk` copy updated to reflect + the new numbers (default 512 GiB / 2 TiB; large 1 TiB / 4 TiB) +- `tests/test_zip_extract.c` — `test_large_profile` advertised-number + assertion updated: `max_total_bytes == 4 TiB` (was 2 TiB) +- `README.md` — both limit tables (ZIP + RAR), the "Tuning the threshold" + snippet, and the `err_extract_entry_too_large` FAQ entry bumped to the + new numbers +- `docs/HANDOVER.md` — `LARGE_FILE_THRESHOLD_BYTES`, the + `k_default_limits` / `k_large_limits` ASCII 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 threads `c->limits` from the engine, + picks up the new defaults for free +- `max_ratio` — both profiles unchanged (500 : 1 default / 1000 : 1 large) +- `check_space()` — unchanged; still the real disk-space guard +- `max_entries` — 200 000 default / 500 000 large, unchanged +- `docs/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 the + `large=1` 1000 : 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_large` see no difference (defaults are strictly + more permissive) +- **3A-game single-file archives now extract silently** — no prompt, no + manual `large=1` API 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 diff --git a/Makefile b/Makefile index 8234952..6f340e4 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,7 @@ THIRD_PARTY_CFLAGS := -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng PS5_TP_OBJS := $(patsubst %.c,ps5-obj/%.o,$(THIRD_PARTY_SRCS)) LINUX_TP_OBJS := $(patsubst %.c,linux-obj/%.o,$(THIRD_PARTY_SRCS)) -CFLAGS := -Oz -fno-asynchronous-unwind-tables -fno-unwind-tables -Wall -Werror -ffunction-sections -fdata-sections -Isrc -Ithird_party/minizip-ng/include -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\" +CFLAGS := -Oz -fno-asynchronous-unwind-tables -fno-unwind-tables -Wall -Werror -ffunction-sections -fdata-sections -Isrc -Ithird_party/minizip-ng/include -Ithird_party/unrar -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\" CFLAGS += `$(PKG_CONFIG) libmicrohttpd --cflags` LDFLAGS := -Wl,--gc-sections LDADD := `$(PKG_CONFIG) libmicrohttpd --libs` diff --git a/README.md b/README.md index f8aaf99..3899599 100644 --- a/README.md +++ b/README.md @@ -184,10 +184,10 @@ Passed as `conflict=` on `/api/extract`: ### Tuning the threshold -The 240 GiB frontend threshold lives in `assets/main.js`: +The 480 GiB frontend threshold lives in `assets/main.js`: ```js -const LARGE_FILE_THRESHOLD_BYTES = 240 * 1024 * 1024 * 1024; +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. @@ -368,10 +368,11 @@ Output is a per-case `check`-style report — **83 checks** on the current `main - **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 256 GiB per - entry / 500:1 ratio. Confirm the large-file prompt (appears for - archives > 240 GiB on disk), split the archive, or pass `large=1` - directly to the API. +- **`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 + pass `large=1` directly to the API. - **`err_extract_unsupported`** — the archive uses a feature the engine cannot handle: encrypted ZIP, encrypted RAR, multi-volume RAR (`.part02+.rar`), very-old RAR 1.4, RAR symlinks / FIFOs, or a file diff --git a/assets/lang-en.js b/assets/lang-en.js index a59596b..1ae7776 100644 --- a/assets/lang-en.js +++ b/assets/lang-en.js @@ -105,7 +105,7 @@ window.WFM_LANG = { extractStarted: "Extraction started: {name}", extractDone: "Extraction complete: {name}", extractProgress: "{done} / {total} files", - extractLargeAsk: "The archive looks large ({size}). Enable the large-file profile?\n\nOK = yes (single file up to 1 TiB, archive total up to 2 TiB)\nCancel = default limits (single file 256 GiB, archive total 1 TiB); this archive may be rejected", + extractLargeAsk: "The archive looks large ({size}). Enable the large-file profile?\n\nOK = yes (single file up to 1 TiB, archive total up to 4 TiB)\nCancel = default limits (single file 512 GiB, archive total 2 TiB); this archive may be rejected", extractLargeActive: "Large-file profile is enabled for this task", extractSelectMainVolume: "Please select the main volume (.rar or .part01.rar)", extractArchivePending: "Preparing to extract {name}", diff --git a/assets/lang-zh.js b/assets/lang-zh.js index c1594d4..f98f0da 100644 --- a/assets/lang-zh.js +++ b/assets/lang-zh.js @@ -105,7 +105,7 @@ window.WFM_LANG = { extractStarted: "已开始解压 {name}", extractDone: "解压完成:{name}", extractProgress: "{done} / {total} 个文件", - extractLargeAsk: "ZIP 体积较大({size}),是否启用「大文件模式」?\n\n确定 = 启用(单文件最大 1 TiB / 总解压最大 2 TiB)\n取消 = 默认限制(单文件 256 GiB / 总解压 1 TiB),可能拒绝此压缩包", + extractLargeAsk: "ZIP 体积较大({size}),是否启用「大文件模式」?\n\n确定 = 启用(单文件最大 1 TiB / 总解压最大 4 TiB)\n取消 = 默认限制(单文件 512 GiB / 总解压 2 TiB),可能拒绝此压缩包", extractLargeActive: "此任务已启用大文件模式", extractSelectMainVolume: "请改选主卷(如 .rar 或 .part01.rar)", extractArchivePending: "正在准备解压 {name}", diff --git a/assets/main.js b/assets/main.js index de621b0..37b60d7 100644 --- a/assets/main.js +++ b/assets/main.js @@ -835,7 +835,13 @@ async function startExtractTask(path, dstDir, conflict, removeSource, name, larg } } -const LARGE_FILE_THRESHOLD_BYTES = 240 * 1024 * 1024 * 1024; +// Threshold above which the web UI prompts the user before extracting. +// Tuned so a typical 3A-game archive (~300 GiB single file) and a PS5 system +// backup (~300 GiB total) extract under the default profile without prompting. +// Files between this threshold and the default max_file_bytes cap (512 GiB) +// still extract silently; larger files require explicit user opt-in via the +// large=1 API flag. +const LARGE_FILE_THRESHOLD_BYTES = 480 * 1024 * 1024 * 1024; function shouldPromptLargeMode(itemSize) { return Number(itemSize || 0) > LARGE_FILE_THRESHOLD_BYTES; diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md index 374614b..1d7e070 100644 --- a/docs/HANDOVER.md +++ b/docs/HANDOVER.md @@ -121,7 +121,7 @@ SDK 的 `target/user/homebrew/` 被 `songl(197609)` 拥有 755,普通 song 写 - `assets/lang-en.js` 和 `lang-zh.js` 新增 `extractLargeAsk` / `extractLargeActive` **前端 UX**: -- ZIP 大于 240 GiB 时弹窗「启用大文件模式?」 +- ZIP 大于 480 GiB 时弹窗「启用大文件模式?」 - 用户点 OK → 传 `large=1` → 引擎走 large profile - 用户点取消 → 走 default profile(多半会被拒绝) diff --git a/src/extract.c b/src/extract.c index 137300c..134674d 100644 --- a/src/extract.c +++ b/src/extract.c @@ -45,6 +45,28 @@ ends_with_ci(const char *path, const char *suffix) { return !strcasecmp(path + path_len - suf_len, suffix); } +/* Progress callback. The engine already throttles reports (200 ms / 1 MiB), + so we can forward each report straight into the shared task state. + Defined before extract_dispatch() so the dispatcher's call site compiles + cleanly under -Werror=implicit-function-declaration. */ +static void +extract_progress(void *userdata, const zipx_progress_t *p) { + file_task_t *task = userdata; + unsigned long long prev_done; + unsigned long long delta; + + pthread_mutex_lock(&g_tasks_lock); + task->entries_total = p->entries_total; + task->entries_done = p->entries_done; + task->total = p->bytes_total; + prev_done = task->done; + pthread_mutex_unlock(&g_tasks_lock); + + delta = p->bytes_done > prev_done ? p->bytes_done - prev_done : 0; + task_update(task, TASK_RUNNING, p->current ? p->current : task->src, + delta, NULL); +} + /* Pick the right engine by the archive file name. Returns ZIPX_ERR_FORMAT for anything that does not look like a supported archive. */ static zipx_status_t @@ -71,26 +93,6 @@ extract_dispatch(file_task_t *task, zipx_conflict_t conflict, } } -/* Progress callback. The engine already throttles reports (200 ms / 1 MiB), - so we can forward each report straight into the shared task state. */ -static void -extract_progress(void *userdata, const zipx_progress_t *p) { - file_task_t *task = userdata; - unsigned long long prev_done; - unsigned long long delta; - - pthread_mutex_lock(&g_tasks_lock); - task->entries_total = p->entries_total; - task->entries_done = p->entries_done; - task->total = p->bytes_total; - prev_done = task->done; - pthread_mutex_unlock(&g_tasks_lock); - - delta = p->bytes_done > prev_done ? p->bytes_done - prev_done : 0; - task_update(task, TASK_RUNNING, p->current ? p->current : task->src, - delta, NULL); -} - static const char * extract_error_code(zipx_status_t status) { switch(status) { diff --git a/src/zip_extract.c b/src/zip_extract.c index ee4fb7b..7d0bd81 100644 --- a/src/zip_extract.c +++ b/src/zip_extract.c @@ -33,22 +33,40 @@ #define ZIPX_PUBLISH_MAX_DEPTH 128 #define ZIPX_SPACE_SLACK_PER_ENTRY 512 +/* Default limits. + * + * Tuned to cover real-world PS5 workloads without prompting: + * - PS5 system backup ZIPs (~200-300 GiB total, individual chunks <64 GiB) + * - 3A-game archives with a single ~300 GiB uncompressed file + * + * Safety against zip bombs is delegated to: + * 1. `check_space()` (statvfs-based real disk space check) before extract + * 2. `max_ratio` below (declared compression ratio cap) + * The size caps here are an early-fail UX guard, not a security boundary. + */ static const zipx_limits_t k_default_limits = { .max_entries = 200000, - .max_total_bytes = 1ULL * 1024 * 1024 * 1024 * 1024, - .max_file_bytes = 256ULL * 1024 * 1024 * 1024, + .max_total_bytes = 2ULL * 1024 * 1024 * 1024 * 1024, + .max_file_bytes = 512ULL * 1024 * 1024 * 1024, .max_ratio = 500, .max_depth = 32, .max_name_len = 255, .max_path_len = 1024 }; -/* Large profile for archives that exceed the safe limits (e.g. 200 GB system - images). The relaxed ratio still rejects obvious bombs; the size caps - require the user to opt in via the web UI before they take effect. */ +/* Large profile for archives that exceed the default cap. + * + * - max_file_bytes = 1 TiB (single uncompressed file) + * - max_total_bytes = 4 TiB (whole archive) + * - max_ratio = 1000 (relaxed ratio cap; check_space still applies) + * + * Requires the user to opt in via the web UI (large=1) before these take + * effect. Default limits must always be strictly smaller than large so the + * large profile is unambiguously a relaxation. + */ static const zipx_limits_t k_large_limits = { .max_entries = 500000, - .max_total_bytes = 2ULL * 1024 * 1024 * 1024 * 1024, + .max_total_bytes = 4ULL * 1024 * 1024 * 1024 * 1024, .max_file_bytes = 1ULL * 1024 * 1024 * 1024 * 1024, .max_ratio = 1000, .max_depth = 32, diff --git a/tests/test_zip_extract.c b/tests/test_zip_extract.c index 93430e5..266dfa0 100644 --- a/tests/test_zip_extract.c +++ b/tests/test_zip_extract.c @@ -559,8 +559,8 @@ test_large_profile(void) { check(large->max_entries == 500000, "max_entries == 500000"); check(large->max_file_bytes == 1ULL * 1024 * 1024 * 1024 * 1024, "max_file_bytes == 1 TiB"); - check(large->max_total_bytes == 2ULL * 1024 * 1024 * 1024 * 1024, - "max_total_bytes == 2 TiB"); + check(large->max_total_bytes == 4ULL * 1024 * 1024 * 1024 * 1024, + "max_total_bytes == 4 TiB"); check(large->max_ratio == 1000, "max_ratio == 1000"); /* Behaviour: medium_bomb.zip is 1 MiB of 0..255 cycled, compressing to