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 <noreply@anthropic.com>
This commit is contained in:
parent
94126943d5
commit
9e255d8110
@ -4225,7 +4225,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
failure: ExtractArchiveFailureInfo,
|
failure: ExtractArchiveFailureInfo,
|
||||||
scope: "hybrid" | "full"
|
scope: "hybrid" | "full"
|
||||||
): number {
|
): 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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4269,8 +4273,24 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasValidSignature) {
|
if (hasValidSignature) {
|
||||||
logger.warn(`Auto-Recovery (${scope}): ${failure.archiveName} uebersprungen - Dateien korrekte Groesse, Archiv-Signatur gueltig (vermutlich falsches Passwort)`);
|
// Check if other archives in this package were successfully extracted.
|
||||||
return 0;
|
// 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(
|
logger.warn(
|
||||||
@ -6864,6 +6884,20 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (!stream.destroyed) {
|
if (!stream.destroyed) {
|
||||||
stream.destroy();
|
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,
|
// 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.
|
// propagate the error so the download is retried instead of marked complete.
|
||||||
if (bodyError) {
|
if (bodyError) {
|
||||||
|
|||||||
@ -1970,11 +1970,61 @@ async function runExternalExtract(
|
|||||||
}
|
}
|
||||||
} catch (legacyError) {
|
} catch (legacyError) {
|
||||||
const legacyText = String((legacyError as Error)?.message || legacyError || "");
|
const legacyText = String((legacyError as Error)?.message || legacyError || "");
|
||||||
const suggestRedownload = jvmCodecError && classifyExtractionError(legacyText) === "crc_error";
|
const legacyCategory = classifyExtractionError(legacyText);
|
||||||
throw withExtractionErrorHints(legacyError, {
|
const isCrcOrWrongPw = legacyCategory === "crc_error" || legacyCategory === "wrong_password";
|
||||||
suggestRedownload,
|
|
||||||
jvmFailureReason: jvmFailureReason || undefined
|
// ── 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 legacyMs = Date.now() - legacyStartedAt;
|
||||||
const extractorName = path.basename(usedCommand).replace(/\.exe$/i, "");
|
const extractorName = path.basename(usedCommand).replace(/\.exe$/i, "");
|
||||||
@ -2145,6 +2195,9 @@ async function runExternalExtractInner(
|
|||||||
throw new Error(lastError || "Entpacken fehlgeschlagen");
|
throw new Error(lastError || "Entpacken fehlgeschlagen");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delay helper for extraction retries (allows file handles to be released on Windows)
|
||||||
|
const extractRetryDelay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
function isZipSafetyGuardError(error: unknown): boolean {
|
function isZipSafetyGuardError(error: unknown): boolean {
|
||||||
const text = String(error || "").toLowerCase();
|
const text = String(error || "").toLowerCase();
|
||||||
return text.includes("path traversal")
|
return text.includes("path traversal")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user