---
title: "Recover files from a corrupted zip in C#"
url: https://recursiveai.org/onearchive/cookbook/zip-recovery/
description: "Repair a corrupted or truncated zip file in C#: read every entry that survives, and get a list of the entries and byte ranges that were lost. A complete program with OneArchive."
version: "1.0.0"
---

# Recover files from a corrupted zip in C#

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.

```shell
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`:

```csharp
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)"}");
```

```text
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 typed `CompressionFormatException`, as above.
- A damaged entry still raises `CompressionFormatException` when 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.Damage` lists each `ZipDamage` in the order found: `SkippedBytes`, `DirectoryUnusable`, `DamagedEntry` or `Truncated`, with the archive offset, the length, the entry name and a reason. `IsRecovered` is true when the list isn’t empty.
- `Archive.Open` and `Archive.Extract` take the same option through `ArchiveOpenOptions.Zip`.
- Recovery is bounded: `MaxRecoveryBytes` (1 GiB by default) caps the bytes it skips or scans, and every `ArchiveLimits` limit 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](https://recursiveai.org/onearchive/docs/zip-recovery/).
