mirror of
https://github.com/LisherSong/ps5-web-file-manager.git
synced 2026-10-06 07:00:28 +02:00
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
299 lines
11 KiB
C
299 lines
11 KiB
C
/*
|
|
* Test driver for the 7z extraction facade (src/sevenz_extract.c).
|
|
*
|
|
* test_sevenz_extract <archive.7z> <out-dir> [password]
|
|
* Extract one archive. Exit status 0 means ZIPX_OK; the shell compares
|
|
* the result against the fixture's source tree byte for byte.
|
|
*
|
|
* test_sevenz_extract --cases <fixtures-dir> <work-dir>
|
|
* Exercise the error and policy paths that need no byte comparison:
|
|
* a missing/wrong password, an encrypted header, conflicts under each
|
|
* policy, cancellation, limits, a missing destination parent, and the
|
|
* guarantee that nothing is published and no staging tree survives a
|
|
* failure.
|
|
*
|
|
* The host build injects tests/posix_compat.h (see run-sevenz-tests.sh), so
|
|
* the engine can stay plain POSIX.
|
|
*/
|
|
|
|
#include <dirent.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include "sevenz_extract.h"
|
|
|
|
#define PASSWORD "Secret123"
|
|
#define PATH_MAX_LOCAL 4096
|
|
|
|
static int g_checks = 0;
|
|
static int g_failures = 0;
|
|
|
|
static void
|
|
check(int ok, const char *what) {
|
|
g_checks++;
|
|
if(!ok) {
|
|
g_failures++;
|
|
printf(" FAIL %s\n", what);
|
|
}
|
|
}
|
|
|
|
static int
|
|
cancel_always(void *userdata) {
|
|
(void)userdata;
|
|
return 1;
|
|
}
|
|
|
|
static int
|
|
has_staging_leftover(const char *dir) {
|
|
DIR *d = opendir(dir);
|
|
struct dirent *ent;
|
|
int found = 0;
|
|
|
|
if(!d) {
|
|
return 0;
|
|
}
|
|
while((ent = readdir(d))) {
|
|
if(!strncmp(ent->d_name, ".wfm-extract-", 13)) {
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
closedir(d);
|
|
return found;
|
|
}
|
|
|
|
static int
|
|
extract_one(const char *archive, const char *dst, const char *password) {
|
|
zipx_result_t r;
|
|
zipx_status_t st = sevenz_extract(archive, dst, ZIPX_CONFLICT_FAIL,
|
|
zipx_default_limits(), NULL, NULL, NULL,
|
|
password, &r);
|
|
|
|
if(st != ZIPX_OK) {
|
|
fprintf(stderr, " %s: %s: %s\n", archive, zipx_status_string(st),
|
|
r.message);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Runs a case that is expected to fail, and checks the status and message. */
|
|
static void
|
|
expect_fail(const char *label, const char *archive, const char *dst,
|
|
const char *password, zipx_conflict_t conflict,
|
|
const zipx_limits_t *limits, zipx_cancel_fn cancel,
|
|
zipx_status_t want, const char *needle) {
|
|
zipx_result_t r;
|
|
zipx_status_t st;
|
|
char buf[256];
|
|
|
|
st = sevenz_extract(archive, dst, conflict, limits, cancel, NULL, NULL,
|
|
password, &r);
|
|
snprintf(buf, sizeof(buf), "%s: status is %s, not %s", label,
|
|
zipx_status_string(st), zipx_status_string(want));
|
|
check(st == want, buf);
|
|
if(needle) {
|
|
snprintf(buf, sizeof(buf), "%s: message mentions \"%s\" (got \"%s\")",
|
|
label, needle, r.message);
|
|
check(strstr(r.message, needle) != NULL, buf);
|
|
}
|
|
}
|
|
|
|
/* Extracts store.7z into `dst` under the given policy, expecting `want`. */
|
|
static void
|
|
policy_case(const char *label, const char *archive, const char *dst,
|
|
zipx_conflict_t conflict, int run, zipx_status_t want) {
|
|
zipx_result_t r;
|
|
zipx_status_t st;
|
|
char buf[256];
|
|
|
|
st = sevenz_extract(archive, dst, conflict, zipx_default_limits(), NULL, NULL,
|
|
NULL, NULL, &r);
|
|
snprintf(buf, sizeof(buf), "%s (run %d): status is %s, not %s", label, run,
|
|
zipx_status_string(st), zipx_status_string(want));
|
|
check(st == want, buf);
|
|
}
|
|
|
|
/* The progress callback the dispatch driver installs fires the UI loop; it has
|
|
to be called multiple times, with monotonic bytes_done and a non-zero
|
|
bytes_total, or the front-end ends up with no spinner. These checks pin
|
|
that down so a refactor that drops the callback returns the test set to
|
|
red instead of "no progress shown" on the device. */
|
|
typedef struct {
|
|
int calls;
|
|
unsigned long long prev_done;
|
|
unsigned long long max_bytes_total;
|
|
unsigned long long max_bytes_done;
|
|
unsigned long long max_entries_total;
|
|
unsigned long long max_entries_done;
|
|
} progress_recorder_t;
|
|
|
|
static void
|
|
recorder_progress(void *userdata, const zipx_progress_t *p) {
|
|
progress_recorder_t *r = userdata;
|
|
|
|
r->calls++;
|
|
if(p->bytes_total > r->max_bytes_total) r->max_bytes_total = p->bytes_total;
|
|
if(p->bytes_done > r->max_bytes_done) r->max_bytes_done = p->bytes_done;
|
|
if((unsigned long long)p->entries_total > r->max_entries_total) {
|
|
r->max_entries_total = (unsigned long long)p->entries_total;
|
|
}
|
|
if((unsigned long long)p->entries_done > r->max_entries_done) {
|
|
r->max_entries_done = (unsigned long long)p->entries_done;
|
|
}
|
|
if(p->bytes_done < r->prev_done) {
|
|
printf(" FAIL progress: bytes_done went backwards (%llu -> %llu)\n",
|
|
r->prev_done, p->bytes_done);
|
|
g_failures++;
|
|
}
|
|
r->prev_done = p->bytes_done;
|
|
}
|
|
|
|
static int
|
|
run_cases(const char *fx, const char *work) {
|
|
char arc[PATH_MAX_LOCAL];
|
|
char dst[PATH_MAX_LOCAL];
|
|
zipx_limits_t tight;
|
|
|
|
mkdir(work, 0777);
|
|
|
|
/* --- passwords ----------------------------------------------------- */
|
|
snprintf(arc, sizeof(arc), "%s/aes.7z", fx);
|
|
snprintf(dst, sizeof(dst), "%s/pw-missing", work);
|
|
mkdir(dst, 0777);
|
|
expect_fail("aes with no password", arc, dst, NULL, ZIPX_CONFLICT_FAIL,
|
|
zipx_default_limits(), NULL, ZIPX_ERR_PASSWORD, "encrypted");
|
|
|
|
snprintf(dst, sizeof(dst), "%s/pw-wrong", work);
|
|
mkdir(dst, 0777);
|
|
expect_fail("aes with the wrong password", arc, dst, "NotThePassword",
|
|
ZIPX_CONFLICT_FAIL, zipx_default_limits(), NULL,
|
|
ZIPX_ERR_PASSWORD, "7zAES");
|
|
{
|
|
char b2[ZIPX_PATH_MAX + 96];
|
|
zipx_result_t r;
|
|
(void)sevenz_extract(arc, dst, ZIPX_CONFLICT_FAIL,
|
|
zipx_default_limits(), NULL, NULL, NULL,
|
|
"NotThePassword", &r);
|
|
snprintf(b2, sizeof(b2),
|
|
"wrong password: detail names the archive (got '%s')", r.detail);
|
|
check(strstr(r.detail, "aes.7z") != NULL, b2);
|
|
}
|
|
check(!has_staging_leftover(work), "no staging tree survives a wrong password");
|
|
|
|
snprintf(dst, sizeof(dst), "%s/pw-ok", work);
|
|
mkdir(dst, 0777);
|
|
check(extract_one(arc, dst, PASSWORD) == 0,
|
|
"aes extracts with the right password");
|
|
|
|
/* --- encrypted header (-mhe=on) ------------------------------------- */
|
|
/* The file names, the folder table and every entry size live inside the
|
|
encrypted header, so this is the case where nothing at all is readable
|
|
without the password -- not even the entry list. */
|
|
snprintf(arc, sizeof(arc), "%s/aeshe.7z", fx);
|
|
|
|
snprintf(dst, sizeof(dst), "%s/he-missing", work);
|
|
mkdir(dst, 0777);
|
|
expect_fail("aeshe with no password", arc, dst, NULL, ZIPX_CONFLICT_FAIL,
|
|
zipx_default_limits(), NULL, ZIPX_ERR_PASSWORD, "mhe=on");
|
|
check(!has_staging_leftover(work), "no staging tree survives a locked header");
|
|
|
|
snprintf(dst, sizeof(dst), "%s/he-wrong", work);
|
|
mkdir(dst, 0777);
|
|
expect_fail("aeshe with the wrong password", arc, dst, "NotThePassword",
|
|
ZIPX_CONFLICT_FAIL, zipx_default_limits(), NULL,
|
|
ZIPX_ERR_PASSWORD, "password");
|
|
|
|
snprintf(dst, sizeof(dst), "%s/he-ok", work);
|
|
mkdir(dst, 0777);
|
|
check(extract_one(arc, dst, PASSWORD) == 0,
|
|
"aeshe extracts and verifies with the right password");
|
|
check(!has_staging_leftover(work), "no staging tree survives an aeshe run");
|
|
|
|
/* --- open failures -------------------------------------------------- */
|
|
snprintf(dst, sizeof(dst), "%s/missing-file", work);
|
|
mkdir(dst, 0777);
|
|
expect_fail("a missing archive", "/no/such/archive.7z", dst, NULL,
|
|
ZIPX_CONFLICT_FAIL, zipx_default_limits(), NULL, ZIPX_ERR_OPEN,
|
|
"cannot open");
|
|
|
|
snprintf(arc, sizeof(arc), "%s/store.7z", fx);
|
|
snprintf(dst, sizeof(dst), "%s/no-parent/deeper", work);
|
|
expect_fail("a destination whose parent is missing", arc, dst, NULL,
|
|
ZIPX_CONFLICT_FAIL, zipx_default_limits(), NULL, ZIPX_ERR_IO,
|
|
"destination parent is missing");
|
|
|
|
/* --- conflict policies ---------------------------------------------- */
|
|
snprintf(dst, sizeof(dst), "%s/policy-fail", work);
|
|
mkdir(dst, 0777);
|
|
policy_case("fail", arc, dst, ZIPX_CONFLICT_FAIL, 1, ZIPX_OK);
|
|
policy_case("fail", arc, dst, ZIPX_CONFLICT_FAIL, 2, ZIPX_ERR_CONFLICT);
|
|
|
|
snprintf(dst, sizeof(dst), "%s/policy-overwrite", work);
|
|
mkdir(dst, 0777);
|
|
policy_case("overwrite", arc, dst, ZIPX_CONFLICT_OVERWRITE, 1, ZIPX_OK);
|
|
policy_case("overwrite", arc, dst, ZIPX_CONFLICT_OVERWRITE, 2, ZIPX_OK);
|
|
|
|
snprintf(dst, sizeof(dst), "%s/policy-merge", work);
|
|
mkdir(dst, 0777);
|
|
policy_case("merge", arc, dst, ZIPX_CONFLICT_MERGE, 1, ZIPX_OK);
|
|
policy_case("merge", arc, dst, ZIPX_CONFLICT_MERGE, 2, ZIPX_OK);
|
|
|
|
/* --- cancellation --------------------------------------------------- */
|
|
snprintf(dst, sizeof(dst), "%s/cancel", work);
|
|
mkdir(dst, 0777);
|
|
expect_fail("a canceled extraction", arc, dst, NULL, ZIPX_CONFLICT_FAIL,
|
|
zipx_default_limits(), cancel_always, ZIPX_ERR_CANCELED, NULL);
|
|
check(!has_staging_leftover(work), "no staging tree survives a cancellation");
|
|
|
|
/* --- limits --------------------------------------------------------- */
|
|
tight = *zipx_default_limits();
|
|
tight.max_entries = 1;
|
|
snprintf(dst, sizeof(dst), "%s/limits", work);
|
|
mkdir(dst, 0777);
|
|
expect_fail("an archive over the entry limit", arc, dst, NULL,
|
|
ZIPX_CONFLICT_FAIL, &tight, NULL, ZIPX_ERR_LIMIT_ENTRIES,
|
|
"more than 1 entries");
|
|
|
|
/* --- progress callback ---------------------------------------------- */
|
|
{
|
|
progress_recorder_t rec = {0};
|
|
zipx_result_t r;
|
|
|
|
snprintf(dst, sizeof(dst), "%s/progress", work);
|
|
mkdir(dst, 0777);
|
|
(void)sevenz_extract(arc, dst, ZIPX_CONFLICT_FAIL,
|
|
zipx_default_limits(), NULL, recorder_progress,
|
|
&rec, NULL, &r);
|
|
check(rec.calls >= 2, "progress callback fires multiple times");
|
|
check(rec.max_bytes_total > 0, "progress reports a non-zero bytes_total");
|
|
check(rec.max_bytes_done > 0,
|
|
"progress reports a non-zero bytes_done during extraction");
|
|
check(rec.max_entries_total >= 1,
|
|
"progress reports a non-zero entries_total");
|
|
check(rec.max_bytes_done >= rec.max_bytes_total * 9 / 10,
|
|
"progress reaches within 90% of the declared total");
|
|
}
|
|
|
|
printf(" cases: %d checks, %d failures\n", g_checks, g_failures);
|
|
return g_failures == 0 ? 0 : 1;
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv) {
|
|
setvbuf(stdout, NULL, _IONBF, 0);
|
|
if(argc >= 4 && !strcmp(argv[1], "--cases")) {
|
|
return run_cases(argv[2], argv[3]);
|
|
}
|
|
if(argc < 3) {
|
|
fprintf(stderr,
|
|
"usage: %s <archive.7z> <out-dir> [password]\n"
|
|
" %s --cases <fixtures-dir> <work-dir>\n",
|
|
argv[0], argv[0]);
|
|
return 2;
|
|
}
|
|
return extract_one(argv[1], argv[2], argc > 3 ? argv[3] : NULL);
|
|
}
|