Open the zip with new ZipReaderOptions { Recovery = true } and read the entries as usual: what survives comes back, and ZipReader.Damage lists every entry and byte range that was lost. The program below cuts a zip short, shows the normal error, then recovers what is left.
A zip’s directory sits at the end of the file, so a download cut short loses it, and with it the whole archive. Recovery mode reads the local headers instead, skips what it can’t parse, and keeps going. It is new in 1.0 and off by default.
dotnet new console -o ZipRecovery && cd ZipRecovery dotnet add package RecursiveAI.OneArchive --version 1.0.0
Put this in Program.cs and run dotnet run:
using RecursiveAI.OneArchive; using RecursiveAI.OneArchive.Zip; using RecursiveAI.OneCompress; // A zip of four day logs, then damaged: a run of bytes overwritten inside one entry, and the download cut short. Directory.CreateDirectory("logs"); for (int d = 1; d <= 4; d++) File.WriteAllText($"logs/day{d}.log", string.Concat(Enumerable.Range(0, 400).Select(i => $"day {d} event {i}: sensor {i % 7} ok\n"))); Archive.Create("logs.zip").Add("logs/").Save(); byte[] zip = File.ReadAllBytes("logs.zip"); byte[] damaged = zip[..(zip.Length * 4 / 5)]; for (int i = 0; i < 16; i++) damaged[damaged.Length / 3 + i] ^= 0xFF; File.WriteAllBytes("damaged.zip", damaged); Console.WriteLine($"logs.zip: {zip.Length:N0} bytes; damaged.zip: {damaged.Length:N0} bytes"); // Without recovery, a damaged archive is a typed error. try { Archive.Extract("damaged.zip", "plain"); } catch (CompressionFormatException e) { Console.WriteLine($"without recovery: {e.Message}"); } // With recovery, read every entry that survives and list what was lost. using var reader = new ZipReader(File.OpenRead("damaged.zip"), new ZipReaderOptions { Recovery = true }); while (reader.GetNextEntry() is { } entry) { if (entry.Kind != ArchiveEntryKind.File) continue; try { using var s = reader.OpenEntryStream(); Console.WriteLine($" ok {entry.Name} ({new StreamReader(s).ReadToEnd().Length:N0} chars)"); } catch (CompressionFormatException) { Console.WriteLine($" damaged {entry.Name}"); } } foreach (ZipDamage d in reader.Damage) Console.WriteLine($"{d.Kind,-17} at byte {d.Offset,5:N0} {d.EntryName ?? "(no entry)"}");
logs.zip: 6,203 bytes; damaged.zip: 4,962 bytes without recovery: zip: invalid data at byte offset 0: expected an end of central directory record (PK\5\6) in the last 65557 bytes; this is not a zip archive, or it is truncated ok logs/day1.log (11,490 chars) damaged logs/day2.log ok logs/day3.log (11,490 chars) damaged logs/day4.log DirectoryUnusable at byte 0 (no entry) DamagedEntry at byte 1,477 logs/day2.log Truncated at byte 4,271 logs/day4.log
Limits
- It reads, it doesn’t rewrite. Bytes that are gone stay gone; to get a clean zip, extract what survived and zip it again.
Good to know
- Without
Recovery, nothing changes: every malformation is a typedCompressionFormatException, as above. - A damaged entry still raises
CompressionFormatExceptionwhen read, so a damaged file is never handed back as a good one. The reader records it and moves on to the next entry. ZipReader.Damagelists eachZipDamagein the order found:SkippedBytes,DirectoryUnusable,DamagedEntryorTruncated, with the archive offset, the length, the entry name and a reason.IsRecoveredis true when the list isn’t empty.Archive.OpenandArchive.Extracttake the same option throughArchiveOpenOptions.Zip.- Recovery is bounded:
MaxRecoveryBytes(1 GiB by default) caps the bytes it skips or scans, and everyArchiveLimitslimit still applies. - Recovery doesn’t repair or rewrite the archive, and it can’t read encrypted entries without the password.
How candidates are found and checked is in zip recovery.