Files
LisherSong--ps5-web-file-ma…/tests/bench_progress.c
T
Songlx516 ba668ade50 release: v1.9.3M -- encrypted archives, plus the UI round that followed
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
2026-09-24 21:07:12 +08:00

162 lines
5.0 KiB
C

/* Prints every extraction progress report, with a monotonic timestamp.
*
* bench_progress <archive> <out-dir> [--mode print|empty|none]
*
* bench_extract answers "how long"; this answers "did the UI move while it
* took that long". A report that sits at entries=0/N for minutes reads to the
* user as a hang even though bytes are still flowing, so being able to see the
* phase/entries/bytes sequence is what turns "the progress bar freezes" into a
* named phase. Format is picked from the suffix, same as bench_extract.
*
* --mode exists to answer a second question: "does reporting itself cost
* time?". The engine's report() helper runs on the hot path -- unrar calls it
* once per decompressed chunk -- and it reads the clock before it decides
* whether to throttle, so the cost is paid even when no report goes out.
* print a callback that formats and prints every report (diagnostic)
* empty a callback that returns immediately (engine cost, no consumer)
* none no callback at all (the engine skips report() entirely)
* Comparing wall time across the three separates "our bookkeeping" from
* "the file I/O we cannot avoid".
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "zip_extract.h"
#include "rar_extract.h"
#include "sevenz_extract.h"
static double g_t0;
static int
has_suffix(const char *path, const char *suffix) {
size_t path_len, suffix_len, i;
if(!path || !suffix) return 0;
path_len = strlen(path);
suffix_len = strlen(suffix);
if(path_len < suffix_len) return 0;
for(i = 0; i < suffix_len; i++) {
char a = path[path_len - suffix_len + i];
char b = suffix[i];
if(a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a');
if(b >= 'A' && b <= 'Z') b = (char)(b - 'A' + 'a');
if(a != b) return 0;
}
return 1;
}
static double
now_seconds(void) {
struct timespec ts;
if(timespec_get(&ts, TIME_UTC) != TIME_UTC) return 0.0;
return (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0;
}
static const char *
phase_name(int phase) {
switch(phase) {
case ZIPX_PHASE_SCAN: return "scan";
case ZIPX_PHASE_EXTRACT: return "extract";
case ZIPX_PHASE_PUBLISH: return "publish";
case ZIPX_PHASE_CLEANUP: return "cleanup";
default: return "?";
}
}
/* Costs exactly what the engine's report() costs, without a consumer. */
static void
on_progress_empty(void *userdata, const zipx_progress_t *p) {
(void)userdata;
(void)p;
}
static void
on_progress(void *userdata, const zipx_progress_t *p) {
(void)userdata;
printf("%8.3f %-7s entries=%llu/%llu bytes=%llu/%llu %s\n",
now_seconds() - g_t0, phase_name(p->phase),
(unsigned long long)p->entries_done,
(unsigned long long)p->entries_total,
(unsigned long long)p->bytes_done,
(unsigned long long)p->bytes_total,
p->current ? p->current : "");
fflush(stdout);
}
int
main(int argc, char **argv) {
zipx_result_t result;
zipx_status_t status;
const char *archive;
const char *out_dir;
const char *format;
const char *mode = "print";
zipx_progress_fn on_report = on_progress;
double elapsed;
int i;
if(argc < 3) {
fprintf(stderr, "usage: %s <archive> <out-dir> [--mode print|empty|none]\n",
argv[0]);
return 2;
}
archive = argv[1];
out_dir = argv[2];
for(i = 3; i < argc; i++) {
if(!strcmp(argv[i], "--mode") && i + 1 < argc) {
mode = argv[++i];
} else {
fprintf(stderr, "unknown argument: %s\n", argv[i]);
return 2;
}
}
if(!strcmp(mode, "empty")) {
on_report = on_progress_empty;
} else if(!strcmp(mode, "none")) {
on_report = NULL;
} else if(strcmp(mode, "print")) {
fprintf(stderr, "unknown --mode: %s\n", mode);
return 2;
}
if(has_suffix(archive, ".rar") || has_suffix(archive, ".part1.rar") ||
has_suffix(archive, ".r00")) {
format = "rar";
} else if(has_suffix(archive, ".zip") || has_suffix(archive, ".z01") ||
has_suffix(archive, ".zip.001")) {
format = "zip";
} else {
format = "7z";
}
memset(&result, 0, sizeof(result));
g_t0 = now_seconds();
if(!strcmp(format, "rar")) {
status = rar_extract(archive, out_dir, ZIPX_CONFLICT_OVERWRITE,
zipx_default_limits(), NULL, on_report, NULL, NULL,
&result);
} else if(!strcmp(format, "zip")) {
status = zipx_extract(archive, out_dir, ZIPX_CONFLICT_OVERWRITE,
zipx_default_limits(), NULL, on_report, NULL, NULL,
&result);
} else {
status = sevenz_extract(archive, out_dir, ZIPX_CONFLICT_OVERWRITE,
zipx_default_limits(), NULL, on_report, NULL,
NULL, &result);
}
elapsed = now_seconds() - g_t0;
printf("---- done: format=%s mode=%s status=%s entries=%llu bytes=%llu wall=%.3fs\n",
format, mode, zipx_status_string(status),
(unsigned long long)result.entries_total,
(unsigned long long)result.bytes_total, elapsed);
return status == ZIPX_OK ? 0 : 1;
}