From 9e255d8110d97f0c7009894d7313fb12af0ead4d Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sun, 8 Mar 2026 00:50:03 +0100 Subject: [PATCH] Fix extraction failures on encrypted RAR5 archives with correct file content - Retry extraction with 2.5s delay on CRC/password errors (Windows file handle race) - Improve auto-recovery: force re-download when known password fails (content corruption) - Expand auto-recovery to wrong_password category for encrypted RAR5 - Add fsync after download for pre-allocated files - Fix permanent extraction failure loop for archives with valid headers but corrupt content Co-Authored-By: Claude Opus 4.6 --- src/main/download-manager.ts | 40 +++++++++++++++++++++-- src/main/extractor.ts | 63 +++++++++++++++++++++++++++++++++--- 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 859d55c..63dfb8d 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -4225,7 +4225,11 @@ export class DownloadManager extends EventEmitter { failure: ExtractArchiveFailureInfo, scope: "hybrid" | "full" ): number { - if (!failure.suggestRedownload || failure.category !== "crc_error") { + // Allow auto-recovery for both crc_error and wrong_password when suggestRedownload is set. + // Encrypted RAR5 archives with corrupt content produce "Checksum error in the encrypted + // file" which is indistinguishable from a wrong-password error. When the JVM extractor + // also failed (suggestRedownload=true), re-downloading is warranted for both categories. + if (!failure.suggestRedownload || (failure.category !== "crc_error" && failure.category !== "wrong_password")) { return 0; } @@ -4269,8 +4273,24 @@ export class DownloadManager extends EventEmitter { } if (hasValidSignature) { - logger.warn(`Auto-Recovery (${scope}): ${failure.archiveName} uebersprungen - Dateien korrekte Groesse, Archiv-Signatur gueltig (vermutlich falsches Passwort)`); - return 0; + // Check if other archives in this package were successfully extracted. + // If a known password worked for siblings but NOT for this archive, + // the file is very likely corrupt despite having a valid header. + const otherItemsExtracted = items.some((item) => + item.status === "completed" && isExtractedLabel(item.fullStatus) + && !archiveItems.includes(item) + ); + if (otherItemsExtracted) { + logger.warn( + `Auto-Recovery (${scope}): ${failure.archiveName} - Signatur gueltig aber ` + + `andere Archive im Paket bereits entpackt (bekanntes Passwort existiert). ` + + `Erzwinge Re-Download aller ${archiveItems.length} Parts (wahrscheinliche Korruption)` + ); + corruptArchiveItems.push(...inspectedArchiveItems); + } else { + logger.warn(`Auto-Recovery (${scope}): ${failure.archiveName} uebersprungen - Dateien korrekte Groesse, Archiv-Signatur gueltig (vermutlich falsches Passwort)`); + return 0; + } } logger.warn( @@ -6864,6 +6884,20 @@ export class DownloadManager extends EventEmitter { if (!stream.destroyed) { stream.destroy(); } + // fsync for pre-allocated files: force OS to flush all pending writes to + // disk so extraction processes opening the file immediately after download + // see the complete data (prevents "Checksum error" on Windows when file + // handles haven't been fully released yet). + if (!bodyError && preAllocated) { + try { + const syncFd = await fs.promises.open(effectiveTargetPath, "r"); + try { + await syncFd.datasync(); + } finally { + await syncFd.close(); + } + } catch { /* best-effort; extraction retry will catch any remaining issues */ } + } // If the body read succeeded but the final flush or stream close failed, // propagate the error so the download is retried instead of marked complete. if (bodyError) { diff --git a/src/main/extractor.ts b/src/main/extractor.ts index 0a83f53..daee8b8 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -1970,11 +1970,61 @@ async function runExternalExtract( } } catch (legacyError) { const legacyText = String((legacyError as Error)?.message || legacyError || ""); - const suggestRedownload = jvmCodecError && classifyExtractionError(legacyText) === "crc_error"; - throw withExtractionErrorHints(legacyError, { - suggestRedownload, - jvmFailureReason: jvmFailureReason || undefined - }); + const legacyCategory = classifyExtractionError(legacyText); + const isCrcOrWrongPw = legacyCategory === "crc_error" || legacyCategory === "wrong_password"; + + // ── Retry once after 2s delay ── + // On Windows, freshly completed downloads may still have file handles not + // fully released by the OS. Encrypted RAR5 headers are especially sensitive: + // even a single unreadable byte causes "Checksum error in the encrypted file" + // at bestPercent=0, indistinguishable from a wrong password. + // A short delay allows the OS to finalise all handles and flush caches. + if (isCrcOrWrongPw && !signal?.aborted) { + const retryDelayMs = 2500; + logger.warn( + `Legacy-Extraktion fehlgeschlagen (${legacyCategory}), Retry nach ${retryDelayMs}ms Delay: ${archiveName}` + ); + await extractRetryDelay(retryDelayMs); + if (!signal?.aborted) { + try { + const retryCmd = usedCommand; + const retryPassword = await runExternalExtractInner( + retryCmd, + archivePath, + effectiveTargetDir, + conflictMode, + passwordCandidates, + onArchiveProgress, + signal, + timeoutMs, + hybridMode, + onPasswordAttempt, + forceFlatMode, + flatModeResult + ); + logger.info(`Legacy-Retry erfolgreich: ${archiveName}`); + password = retryPassword; + usedCommand = retryCmd; + } catch (retryError) { + const retryText = String((retryError as Error)?.message || retryError || ""); + const retryCategory = classifyExtractionError(retryText); + logger.warn(`Legacy-Retry ebenfalls fehlgeschlagen (${retryCategory}): ${archiveName}`); + const suggestRedownload = jvmCodecError && (retryCategory === "crc_error" || retryCategory === "wrong_password"); + throw withExtractionErrorHints(retryError, { + suggestRedownload, + jvmFailureReason: jvmFailureReason || undefined + }); + } + } else { + throw legacyError; + } + } else { + const suggestRedownload = jvmCodecError && isCrcOrWrongPw; + throw withExtractionErrorHints(legacyError, { + suggestRedownload, + jvmFailureReason: jvmFailureReason || undefined + }); + } } const legacyMs = Date.now() - legacyStartedAt; const extractorName = path.basename(usedCommand).replace(/\.exe$/i, ""); @@ -2145,6 +2195,9 @@ async function runExternalExtractInner( throw new Error(lastError || "Entpacken fehlgeschlagen"); } +// Delay helper for extraction retries (allows file handles to be released on Windows) +const extractRetryDelay = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); + function isZipSafetyGuardError(error: unknown): boolean { const text = String(error || "").toLowerCase(); return text.includes("path traversal")