fix(7z): keep the spinner alive on small archives; surface password errors with the archive path

UI regression: a 7z with fewer than 4096 entries spun a "scanning archive"
label and never replaced it until extraction had been running long enough
for the byte-level callback to fire, on a slow PS5 that meant "looks hung".
Lower the per-entry barrier in scan_entries to 256 entries, force a final
report after the scan so entries_total / bytes_total always reach the UI,
and force a report at the start of precheck_folders so the throttled
"between scan end and first extraction byte" window is not silent either.

Error regression: when the password was wrong the engine returned
ZIPX_ERR_PASSWORD with an empty detail field; extract_set_error copied it
into task->error_arg verbatim, so the i18n template's {arg} expanded to ""
and the user saw "Wrong password: ". Carry the original archive path into
the error detail so the toast reads "Wrong password: myfile.7z".

Languages: drop the stale "only ZIP/RAR" line from err_extract_unsupported
(7z has been supported since v1.9), and add the new err_extract_password
key in both en/zh.

Tests: lock down both regressions.
  * progress recorder: bytes_done monotonic, total > 0, final byte count
    reaches >= 90% of the declared total.
  * wrong password: r.detail contains the archive basename.

7z 28 + ZIP 108 + RAR 27 = 163 checks, 0 failures.
This commit is contained in:
Songlx516 committed 2026-09-13 10:16:53 +08:00
1 parent 27eec8c399
commit 112f8a6b72
4 files changed
+87 -5

No files matched your search

+2 -1
View File
@@ -180,7 +180,8 @@ window.WFM_LANG = {
err_destination_must_be_directory: "Destination must be a folder for multiple items",
err_extract_open_failed: "Cannot open archive: {arg}",
err_extract_corrupt: "Archive is corrupt or incomplete: {arg}",
err_extract_unsupported: "Unsupported archive (only unencrypted plain ZIP and single-volume RAR are supported): {arg}",
err_extract_unsupported: "Unsupported archive (only plain ZIP, single-volume RAR, and 7z are supported): {arg}",
err_extract_password: "Wrong password or the archive is not encrypted with the one supplied: {arg}",
err_extract_unsafe_name: "Archive contains an unsafe path: {arg}",
err_extract_special_entry: "Archive contains an unsupported special file: {arg}",
err_extract_duplicate: "Archive contains duplicate entries: {arg}",
+2 -1
View File
@@ -180,7 +180,8 @@ window.WFM_LANG = {
err_destination_must_be_directory: "多个项目的目标必须是目录",
err_extract_open_failed: "无法打开压缩包: {arg}",
err_extract_corrupt: "压缩包损坏或不完整: {arg}",
err_extract_unsupported: "不支持的压缩包(仅支持未加密的普通 ZIP 与单卷 RAR): {arg}",
err_extract_unsupported: "不支持的压缩包(仅支持未加密的普通 ZIP、单卷 RAR,以及 7z):{arg}",
err_extract_password: "密码错误,或压缩包未使用所提供的密码加密: {arg}",
err_extract_unsafe_name: "压缩包包含不安全的路径: {arg}",
err_extract_special_entry: "压缩包包含不支持的特殊文件: {arg}",
err_extract_duplicate: "压缩包包含重复条目: {arg}",
+18 -3
View File
@@ -76,6 +76,7 @@ typedef struct {
void *userdata;
zipx_result_t *result;
const char *password;
const char *sevenz_path; /* original archive path, surfaced in errors */
uint64_t entries_total;
uint64_t entries_done;
uint64_t bytes_total;
@@ -858,7 +859,9 @@ scan_entries(const CSzArEx *db, szx_ctx_t *c) {
(unsigned long long)c->limits.max_entries);
break;
}
if(c->entries_total % 4096 == 0) {
/* Once per ~256 entries is plenty for a header walk that only needs to
keep the spinner alive; szx_report() also rate-limits at 200 ms. */
if(c->entries_total % 256 == 0) {
if(szx_canceled(c)) {
ret = szx_fail(c, ZIPX_ERR_CANCELED, name, NULL);
break;
@@ -867,6 +870,11 @@ scan_entries(const CSzArEx *db, szx_ctx_t *c) {
}
}
/* For archives below the per-N-entry report threshold the scan never fires a
throttled callback; push the final totals out so the UI sees total/entries
populated before extraction begins. */
szx_report(c, ZIPX_PHASE_SCAN, NULL, 1);
szx_nameset_free(&set);
return ret;
}
@@ -904,6 +912,10 @@ static int
precheck_folders(const CSzArEx *db, szx_ctx_t *c) {
UInt32 folder;
/* The decode stack for every folder has to round-trip here before the first
byte is written, so a multi-thousand-folder archive does not look stalled
between the entry scan ending and extraction starting. */
szx_report(c, ZIPX_PHASE_SCAN, "validating folders", 1);
for(folder = 0; folder < db->db.NumFolders; folder++) {
uint64_t pack_positions[SZ_CHAIN_MAX_STREAMS + 1];
UInt32 pack_count = 0;
@@ -1197,9 +1209,11 @@ szx_sink_write(void *ctx, const void *data, size_t size) {
static int
szx_decode_error(szx_ctx_t *c, UInt32 folder, const sz_chain_err_t *derr,
const char *desc, int encrypted) {
const char *detail = encrypted ? c->sevenz_path : NULL;
switch(derr->status) {
case SZ_CHAIN_ERR_PASSWORD:
return szx_fail(c, ZIPX_ERR_PASSWORD, NULL, "folder %u (%s): %s",
return szx_fail(c, ZIPX_ERR_PASSWORD, detail, "folder %u (%s): %s",
(unsigned)folder, desc, derr->message);
case SZ_CHAIN_ERR_METHOD:
case SZ_CHAIN_ERR_LAYOUT:
@@ -1216,7 +1230,7 @@ szx_decode_error(szx_ctx_t *c, UInt32 folder, const sz_chain_err_t *derr,
(unsigned)folder, desc, derr->message);
case SZ_CHAIN_ERR_DATA:
if(encrypted) {
return szx_fail(c, ZIPX_ERR_PASSWORD, NULL,
return szx_fail(c, ZIPX_ERR_PASSWORD, detail,
"folder %u (%s): %s (the password is wrong, or the "
"archive is damaged)",
(unsigned)folder, desc, derr->message);
@@ -1585,6 +1599,7 @@ sevenz_extract(const char *sevenz_path, const char *dst_dir,
c->progress = progress;
c->userdata = userdata;
c->password = password;
c->sevenz_path = sevenz_path;
snprintf(dst_copy, sizeof(dst_copy), "%s", dst_dir);
{
+65
View File
@@ -116,6 +116,41 @@ policy_case(const char *label, const char *archive, const char *dst,
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];
@@ -136,6 +171,16 @@ run_cases(const char *fx, const char *work) {
expect_fail("aes with the wrong password", arc, dst, "NotThePassword",
ZIPX_CONFLICT_FAIL, zipx_default_limits(), NULL,
ZIPX_ERR_PASSWORD, "7zAES");
{
char b2[256];
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);
@@ -195,6 +240,26 @@ run_cases(const char *fx, const char *work) {
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;
}