Commit Graph
21 Commits
Author SHA1 Message Date
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.
v1.8.3
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.
v1.8.2
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
v1.8.1
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.
v1.8
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