Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
68 changes: 51 additions & 17 deletions src/SharpCompress/Common/Zip/ZipEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ public class ZipEntry : Entry
{
private readonly ZipFilePart? _filePart;

// WinZip AES extra data constants
private const int MinimumWinZipAesExtraDataLength = 7;
private const int WinZipAesCompressionMethodOffset = 5;

internal ZipEntry(ZipFilePart? filePart, IReaderOptions readerOptions)
: base(readerOptions)
{
Expand All @@ -33,24 +37,54 @@ internal ZipEntry(ZipFilePart? filePart, IReaderOptions readerOptions)
CreatedTime = times?.UnicodeTimes.Item3;
}

public override CompressionType CompressionType =>
_filePart?.Header.CompressionMethod switch
public override CompressionType CompressionType
{
get
{
var compressionMethod = GetActualCompressionMethod();
return compressionMethod switch
{
ZipCompressionMethod.BZip2 => CompressionType.BZip2,
ZipCompressionMethod.Deflate => CompressionType.Deflate,
ZipCompressionMethod.Deflate64 => CompressionType.Deflate64,
ZipCompressionMethod.LZMA => CompressionType.LZMA,
ZipCompressionMethod.PPMd => CompressionType.PPMd,
ZipCompressionMethod.None => CompressionType.None,
ZipCompressionMethod.Shrink => CompressionType.Shrink,
ZipCompressionMethod.Reduce1 => CompressionType.Reduce1,
ZipCompressionMethod.Reduce2 => CompressionType.Reduce2,
ZipCompressionMethod.Reduce3 => CompressionType.Reduce3,
ZipCompressionMethod.Reduce4 => CompressionType.Reduce4,
ZipCompressionMethod.Explode => CompressionType.Explode,
ZipCompressionMethod.ZStandard => CompressionType.ZStandard,
_ => CompressionType.Unknown,
};
}
}

private ZipCompressionMethod GetActualCompressionMethod()
{
if (_filePart?.Header.CompressionMethod != ZipCompressionMethod.WinzipAes)
{
ZipCompressionMethod.BZip2 => CompressionType.BZip2,
ZipCompressionMethod.Deflate => CompressionType.Deflate,
ZipCompressionMethod.Deflate64 => CompressionType.Deflate64,
ZipCompressionMethod.LZMA => CompressionType.LZMA,
ZipCompressionMethod.PPMd => CompressionType.PPMd,
ZipCompressionMethod.None => CompressionType.None,
ZipCompressionMethod.Shrink => CompressionType.Shrink,
ZipCompressionMethod.Reduce1 => CompressionType.Reduce1,
ZipCompressionMethod.Reduce2 => CompressionType.Reduce2,
ZipCompressionMethod.Reduce3 => CompressionType.Reduce3,
ZipCompressionMethod.Reduce4 => CompressionType.Reduce4,
ZipCompressionMethod.Explode => CompressionType.Explode,
ZipCompressionMethod.ZStandard => CompressionType.ZStandard,
_ => CompressionType.Unknown,
};
return _filePart?.Header.CompressionMethod ?? ZipCompressionMethod.None;
}
Comment on lines +67 to +70
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

GetActualCompressionMethod() returns ZipCompressionMethod.None when _filePart is null, which changes the previous behavior of reporting CompressionType.Unknown in that case (old code switched on _filePart?.Header.CompressionMethod). If a ZipEntry can ever be constructed without a ZipFilePart, consider returning ZipCompressionMethod.WinzipAes/None only when you can prove the method, otherwise keep the prior Unknown behavior (e.g., return a sentinel that maps to CompressionType.Unknown).

Copilot uses AI. Check for mistakes.

// For WinZip AES, the actual compression method is stored in the extra data
var aesExtraData = _filePart.Header.Extra.FirstOrDefault(x =>
x.Type == ExtraDataType.WinZipAes
);

if (aesExtraData is null || aesExtraData.DataBytes.Length < MinimumWinZipAesExtraDataLength)
{
return ZipCompressionMethod.WinzipAes;
}

// The compression method is at offset 5 in the extra data
return (ZipCompressionMethod)
System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(
aesExtraData.DataBytes.AsSpan(WinZipAesCompressionMethodOffset)
);
Comment on lines +72 to +86
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

For WinZip AES entries, this reads the compression method from the extra field but does not validate the extra data (length == 7, vendor version, vendor ID) and it uses FirstOrDefault rather than SingleOrDefault. This can make Entry.CompressionType disagree with extraction behavior, since ZipFilePart.CreateDecompressionStream validates these fields and will throw on malformed or duplicate AES extra data. To keep behavior consistent, mirror the same validation and selection logic here and fall back to Unknown when the AES metadata is invalid.

Suggested change
// For WinZip AES, the actual compression method is stored in the extra data
var aesExtraData = _filePart.Header.Extra.FirstOrDefault(x =>
x.Type == ExtraDataType.WinZipAes
);
if (aesExtraData is null || aesExtraData.DataBytes.Length < MinimumWinZipAesExtraDataLength)
{
return ZipCompressionMethod.WinzipAes;
}
// The compression method is at offset 5 in the extra data
return (ZipCompressionMethod)
System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(
aesExtraData.DataBytes.AsSpan(WinZipAesCompressionMethodOffset)
);
// For WinZip AES, the actual compression method is stored in the extra data.
// Mirror the validation logic used during extraction so that behavior is consistent.
var aesExtraDataList = _filePart.Header.Extra
.Where(x => x.Type == ExtraDataType.WinZipAes)
.ToList();
// If there is not exactly one AES extra data record, treat the metadata as invalid.
if (aesExtraDataList.Count != 1)
{
// We do not know the actual compression method; keep WinzipAes so higher layers
// can treat the compression type as unknown.
return ZipCompressionMethod.WinzipAes;
}
var aesExtraData = aesExtraDataList[0];
var data = aesExtraData.DataBytes;
// WinZip AES extra data layout (7 bytes total):
// 0-1: Vendor version (1 or 2)
// 2-3: Vendor ID ("AE" -> 0x4541 little-endian)
// 4 : AES strength (ignored here)
// 5-6: Actual compression method
if (data is null || data.Length != MinimumWinZipAesExtraDataLength)
{
return ZipCompressionMethod.WinzipAes;
}
var span = data.AsSpan();
ushort vendorVersion =
System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(0, 2));
ushort vendorId =
System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(2, 2));
// Only accept known vendor versions and the WinZip AES vendor ID ("AE").
bool isSupportedVendorVersion = vendorVersion == 1 || vendorVersion == 2;
const ushort WinZipAesVendorId = 0x4541; // "AE" in little-endian
if (!isSupportedVendorVersion || vendorId != WinZipAesVendorId)
{
return ZipCompressionMethod.WinzipAes;
}
// The actual compression method is at offset 5 in the extra data.
ushort method =
System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(
span.Slice(WinZipAesCompressionMethodOffset, sizeof(ushort))
);
return (ZipCompressionMethod)method;

Copilot uses AI. Check for mistakes.
}

public override long Crc => _filePart?.Header.Crc ?? 0;

Expand Down
50 changes: 50 additions & 0 deletions tests/SharpCompress.Test/Zip/ZipArchiveTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,56 @@ public void Zip_Deflate_WinzipAES_MultiOpenEntryStream()
}
}

[Fact]
public void Zip_WinzipAES_CompressionType()
{
// Test that WinZip AES encrypted entries correctly report their compression type
using var deflateArchive = ZipArchive.OpenArchive(
Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip"),
new ReaderOptions { Password = "test" }
);
foreach (var entry in deflateArchive.Entries.Where(x => !x.IsDirectory))
{
Assert.True(entry.IsEncrypted);
Assert.Equal(CompressionType.Deflate, entry.CompressionType);
}

using var lzmaArchive = ZipArchive.OpenArchive(
Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.WinzipAES.zip"),
new ReaderOptions { Password = "test" }
);
foreach (var entry in lzmaArchive.Entries.Where(x => !x.IsDirectory))
{
Assert.True(entry.IsEncrypted);
Assert.Equal(CompressionType.LZMA, entry.CompressionType);
}
}

[Fact]
public void Zip_Pkware_CompressionType()
{
// Test that Pkware encrypted entries correctly report their compression type
using var deflateArchive = ZipArchive.OpenArchive(
Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.pkware.zip"),
new ReaderOptions { Password = "test" }
);
foreach (var entry in deflateArchive.Entries.Where(x => !x.IsDirectory))
{
Assert.True(entry.IsEncrypted);
Assert.Equal(CompressionType.Deflate, entry.CompressionType);
}

using var bzip2Archive = ZipArchive.OpenArchive(
Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.pkware.zip"),
new ReaderOptions { Password = "test" }
);
foreach (var entry in bzip2Archive.Entries.Where(x => !x.IsDirectory))
{
Assert.True(entry.IsEncrypted);
Assert.Equal(CompressionType.BZip2, entry.CompressionType);
}
}

[Fact]
public void Zip_Read_Volume_Comment()
{
Expand Down
4 changes: 2 additions & 2 deletions tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ await Assert.ThrowsAsync<NotSupportedException>(async () =>
{
if (!reader.Entry.IsDirectory)
{
Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType);
Assert.Equal(CompressionType.LZMA, reader.Entry.CompressionType);
await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH);
}
}
Expand Down Expand Up @@ -252,7 +252,7 @@ public async ValueTask Zip_Deflate_WinzipAES_Read_Async()
{
if (!reader.Entry.IsDirectory)
{
Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType);
Assert.Equal(CompressionType.Deflate, reader.Entry.CompressionType);
await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH);
}
}
Expand Down
4 changes: 2 additions & 2 deletions tests/SharpCompress.Test/Zip/ZipReaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public void Zip_LZMA_WinzipAES_Read() =>
{
if (!reader.Entry.IsDirectory)
{
Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType);
Assert.Equal(CompressionType.LZMA, reader.Entry.CompressionType);
reader.WriteEntryToDirectory(SCRATCH_FILES_PATH);
}
}
Expand All @@ -223,7 +223,7 @@ public void Zip_Deflate_WinzipAES_Read()
{
if (!reader.Entry.IsDirectory)
{
Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType);
Assert.Equal(CompressionType.Deflate, reader.Entry.CompressionType);
reader.WriteEntryToDirectory(SCRATCH_FILES_PATH);
}
}
Expand Down
Loading