Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion FORMATS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
| 7Zip (4) | LZMA, LZMA2, BZip2, PPMd, BCJ, BCJ2, Deflate | Decompress | SevenZipArchive | N/A | N/A |

1. SOLID Rars are only supported in the RarReader API.
2. Zip format supports pkware and WinzipAES encryption. However, encrypted LZMA is not supported. Zip64 reading/writing is supported but only with seekable streams as the Zip spec doesn't support Zip64 data in post data descriptors. Deflate64 is only supported for reading.
2. Zip format supports pkware and WinzipAES encryption. However, encrypted LZMA is not supported. Zip64 reading/writing is supported but only with seekable streams as the Zip spec doesn't support Zip64 data in post data descriptors. Deflate64 is only supported for reading. Multi-volume/split ZIP archives require ZipArchive (seekable streams) as ZipReader cannot seek across volume files.
3. The Tar format requires a file size in the header. If no size is specified to the TarWriter and the stream is not seekable, then an exception will be thrown.
4. The 7Zip format doesn't allow for reading as a forward-only stream so 7Zip is only supported through the Archive API
5. LZip has no support for extra data like the file name or timestamp. There is a default filename used when looking at the entry Key on the archive.
Expand Down
6 changes: 6 additions & 0 deletions src/SharpCompress/Readers/Zip/ZipReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ protected override IEnumerable<ZipEntry> GetEntries(Stream stream)
);
}
break;
// DirectoryEntry headers in the central directory are intentionally skipped.
// In streaming mode, we can only read forward, and DirectoryEntry headers
// reference LocalEntry headers that have already been processed. The file
// data comes from LocalEntry headers, not DirectoryEntry headers.
// For multi-volume ZIPs where file data spans multiple files, use ZipArchive
// instead, which requires seekable streams.
Copy link

Copilot AI Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment explaining DirectoryEntry behavior is placed immediately before the DirectoryEnd case, which is confusing. It should be placed after the break; on line 77 but before the switch statement ends, or as a comment within the switch explaining the missing case. Consider adding it as:

                    break;
                case ZipHeaderType.DirectoryEntry:
                    // DirectoryEntry headers in the central directory are intentionally skipped.
                    // In streaming mode, we can only read forward, and DirectoryEntry headers
                    // reference LocalEntry headers that have already been processed. The file
                    // data comes from LocalEntry headers, not DirectoryEntry headers.
                    // For multi-volume ZIPs where file data spans multiple files, use ZipArchive
                    // instead, which requires seekable streams.
                    break;
                case ZipHeaderType.DirectoryEnd:

This makes it clear the comment applies to the DirectoryEntry case, not the DirectoryEnd case.

Suggested change
// DirectoryEntry headers in the central directory are intentionally skipped.
// In streaming mode, we can only read forward, and DirectoryEntry headers
// reference LocalEntry headers that have already been processed. The file
// data comes from LocalEntry headers, not DirectoryEntry headers.
// For multi-volume ZIPs where file data spans multiple files, use ZipArchive
// instead, which requires seekable streams.
case ZipHeaderType.DirectoryEntry:
// DirectoryEntry headers in the central directory are intentionally skipped.
// In streaming mode, we can only read forward, and DirectoryEntry headers
// reference LocalEntry headers that have already been processed. The file
// data comes from LocalEntry headers, not DirectoryEntry headers.
// For multi-volume ZIPs where file data spans multiple files, use ZipArchive
// instead, which requires seekable streams.
break;

Copilot uses AI. Check for mistakes.
case ZipHeaderType.DirectoryEnd:
{
yield break;
Expand Down
39 changes: 39 additions & 0 deletions tests/SharpCompress.Test/Zip/ZipReaderTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.IO;
Expand Down Expand Up @@ -397,4 +399,41 @@ public void Zip_Uncompressed_Encrypted_Read()
Assert.Equal("second.txt", reader.Entry.Key);
Assert.Equal(197, reader.Entry.Size);
}

[Fact]
public void ZipReader_Returns_Same_Entries_As_ZipArchive()
{
// Verifies that ZipReader and ZipArchive return the same entries
// for standard single-volume ZIP files. Both process entries from
// LocalEntry headers, while ZipArchive also reads DirectoryEntry
// headers from the central directory.
var testFiles = new[] { "Zip.none.zip", "Zip.deflate.zip", "Zip.none.issue86.zip" };

foreach (var testFile in testFiles)
{
var path = Path.Combine(TEST_ARCHIVES_PATH, testFile);

var readerKeys = new List<string>();
using (var stream = File.OpenRead(path))
using (var reader = ZipReader.Open(stream))
{
while (reader.MoveToNextEntry())
{
readerKeys.Add(reader.Entry.Key!);
}
}

var archiveKeys = new List<string>();
using (var archive = Archives.Zip.ZipArchive.Open(path))
{
foreach (var entry in archive.Entries)
{
archiveKeys.Add(entry.Key!);
}
}

Assert.Equal(archiveKeys.Count, readerKeys.Count);
Assert.Equal(archiveKeys.OrderBy(k => k), readerKeys.OrderBy(k => k));
}
}
}