diff --git a/assets/lang-en.js b/assets/lang-en.js index 7fea1bb..544c948 100644 --- a/assets/lang-en.js +++ b/assets/lang-en.js @@ -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}", diff --git a/assets/lang-zh.js b/assets/lang-zh.js index d6a6700..5266fa6 100644 --- a/assets/lang-zh.js +++ b/assets/lang-zh.js @@ -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}", diff --git a/src/sevenz_extract.c b/src/sevenz_extract.c index a387a32..e9d7a61 100644 --- a/src/sevenz_extract.c +++ b/src/sevenz_extract.c @@ -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); { diff --git a/tests/test_sevenz_extract.c b/tests/test_sevenz_extract.c index 8c32ec4..618801f 100644 --- a/tests/test_sevenz_extract.c +++ b/tests/test_sevenz_extract.c @@ -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; }