Fix: stille Mid-File-Korruption beim Resume nach Verbindungsabbruch (known-total) + Rewind-Haertung (Runde-8-Audit)
RANGE-1 (HIGH, adversarisch 3/3): Wenn ein Debrid-Server beim Socket-Abbruch einen Garbage-Block in den Body schreibt UND das im LETZTEN inneren Versuch passiert, wurde der Tail nicht zurueckgespult: der `attempt < maxAttempts`-Guard verhindert das Vormerken, resumeRewindBytesNextAttempt ist funktions-lokal und ueberlebt den downloadToFile-Re-Entry nicht, und der Outer-Handler nimmt fuer terminated-class den generic-retry-Zweig (loescht die Teil-Datei NICHT). Beim Fresh-Link-Resume wurden die echten Bytes NACH dem Garbage angehaengt → eine Datei mit exakt korrekter Laenge, die die reine Laengen-Pruefung (validateDownloadedFileCompletion) besteht; bei manifestlosen .mkv/.mp4 ohne Pruefsumme fiel die Korruption nie auf. Fix: am Exhaustion-Punkt vor dem Throw den verdaechtigen Tail zurueckspulen (truncate auf written - RESUME_REWIND_BYTES, downloadedBytes ZUERST gesetzt, damit der binary-Re-Entry-prealloc-reconcile auch bei fehlgeschlagenem truncate greift). GEGATED auf bekannte Groesse (totalBytes != null && > 0): dann ist rewound size < totalBytes = minBytes, sodass tryFinalizeItemFromDisk die gekuerzte Datei ablehnt und der Re-Entry den Garbage sauber ueberschreibt. Ehrliche Einordnung: behoben fuer Medien mit bekannter Groesse (Debrid liefert fast immer fileSize). Der seltene Fall OHNE Groessenangabe behaelt eine separate, schon vorher bestehende stille Korruption unter dem dokumentierten Size-only-Validation-Blindspot — durch dieses Rueckspulen nicht loesbar (kein Laengensignal; nur ein Hard-Reset wuerde helfen, groesserer separater Eingriff). Bewusst NICHT angefasst, um keinen vierten Bug einzubauen. REWIND-TRUNCATE-FAIL (MED, 3/3): Das `finally` setzte resumeRewindBytesNextAttempt=0 unbedingt — auch wenn die Rewind-truncate warf (transienter win32-EBUSY/AV-Lock) → Garbage-Tail blieb, Flag gecleart, nie erneut versucht. Fix: Reset nur noch im Success-Branch, sodass ein fehlgeschlagenes Rueckspulen im naechsten Versuch erneut probiert wird. PREALLOC-ZEROS-ACCEPTED (MED, schwaechste) dokumentiert nicht gefixt: dominante Medien/Archive sind immun (footprint-threshold 0); narrow non-binary-Conjunction. Advisor vor Implementierung konsultiert (HIGH-Hot-Path); Check A (totalBytes-null finalisiert die gekuerzte Datei) am echten Code verifiziert und der Fix entsprechend auf known-total gegated. Rot-bewiesen: Cross-Call-Integrationstest (final-attempt Garbage-Inject + Drop → Exhaustion → queueRetry → 2. downloadToFile via Fresh-Link → byte-genaue Inhaltsgleichheit). Ohne den Fix ist die Laengen-Assertion gruen und die Inhalts-Assertion rot (exakt-laengen-Korruption) — genau die RANGE-1-Signatur. Volle Suite 885 gruen, tsc unveraendert (6).
This commit is contained in:
@@ -9633,6 +9633,7 @@ export class DownloadManager extends EventEmitter {
|
||||
await fs.promises.truncate(effectiveTargetPath, resumeStart);
|
||||
existingBytes = resumeStart;
|
||||
item.downloadedBytes = Math.min(item.downloadedBytes, existingBytes);
|
||||
resumeRewindBytesNextAttempt = 0;
|
||||
logAttemptEvent("WARN", "Resume-Schutz aktiv: Teil-Datei vor Retry zurueckgespult", {
|
||||
attempt,
|
||||
previousBytes,
|
||||
@@ -9646,8 +9647,6 @@ export class DownloadManager extends EventEmitter {
|
||||
rewindBytes,
|
||||
error: compactErrorText(rewindError)
|
||||
});
|
||||
} finally {
|
||||
resumeRewindBytesNextAttempt = 0;
|
||||
}
|
||||
} else if (resumeRewindBytesNextAttempt > 0) {
|
||||
resumeRewindBytesNextAttempt = 0;
|
||||
@@ -10593,6 +10592,29 @@ export class DownloadManager extends EventEmitter {
|
||||
await sleep(retryDelayWithJitter(attempt, 250));
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
item.totalBytes != null && item.totalBytes > 0
|
||||
&& written > existingBytes
|
||||
&& shouldRewindResumeTail(normalizedLastError)
|
||||
) {
|
||||
const rewindTarget = Math.max(0, written - RESUME_REWIND_BYTES);
|
||||
item.downloadedBytes = Math.min(item.downloadedBytes, rewindTarget);
|
||||
try {
|
||||
await fs.promises.truncate(effectiveTargetPath, rewindTarget);
|
||||
logAttemptEvent("WARN", "Resume-Schutz: letzter Versuch vor Linkerneuerung zurueckgespult", {
|
||||
attempt,
|
||||
written,
|
||||
rewindTarget
|
||||
});
|
||||
} catch (rewindError) {
|
||||
logAttemptEvent("WARN", "Resume-Schutz: finales Rueckspulen fehlgeschlagen", {
|
||||
attempt,
|
||||
written,
|
||||
rewindTarget,
|
||||
error: compactErrorText(rewindError)
|
||||
});
|
||||
}
|
||||
}
|
||||
if (maxAttemptsBySetting > maxAttempts) {
|
||||
const exhaustedError = existingBytes > 0 && normalizedLastError.startsWith("download_underflow:")
|
||||
? `resume_download_underflow:${normalizedLastError.slice("download_underflow:".length)}`
|
||||
|
||||
Reference in New Issue
Block a user