diff --git a/Directory.Build.props b/Directory.Build.props
index 277c525f0..6ff916b69 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -11,5 +11,6 @@
true
true
true
+ ${NoWarn};IDE0051
diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs
index 74be64fc3..d68b8a877 100644
--- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs
+++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs
@@ -86,55 +86,34 @@ public override async ValueTask ReadAsync(
private void ReadTransformBlocks(Span buffer, int count)
{
var posn = 0;
- var last = count;
+ var remaining = count;
- while (posn < buffer.Length && posn < last)
+ while (posn < buffer.Length && remaining > 0)
{
- var n = ReadTransformOneBlock(buffer, posn, last);
+ var n = ReadTransformOneBlock(buffer, posn, remaining);
posn += n;
+ remaining -= n;
}
}
- private int ReadTransformOneBlock(Span buffer, int offset, int last)
+ private int ReadTransformOneBlock(Span buffer, int offset, int remaining)
{
- if (_isFinalBlock)
+ if (_counterOutOffset == BLOCK_SIZE_IN_BYTES)
{
- throw new ArchiveOperationException();
+ FillCounterOut();
}
- var bytesRemaining = last - offset;
- var bytesToRead =
- (bytesRemaining > BLOCK_SIZE_IN_BYTES) ? BLOCK_SIZE_IN_BYTES : bytesRemaining;
-
- // update the counter
- System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(_counter, _nonce++);
-
- // Determine if this is the final block
- if ((bytesToRead == bytesRemaining) && (_totalBytesLeftToRead == 0))
- {
- _counterOut = _transform.TransformFinalBlock(_counter, 0, BLOCK_SIZE_IN_BYTES);
- _isFinalBlock = true;
- }
- else
- {
- _transform.TransformBlock(
- _counter,
- 0, // offset
- BLOCK_SIZE_IN_BYTES,
- _counterOut,
- 0
- ); // offset
- }
-
- XorInPlace(buffer, offset, bytesToRead);
- return bytesToRead;
+ var bytesToXor = Math.Min(BLOCK_SIZE_IN_BYTES - _counterOutOffset, remaining);
+ XorInPlace(buffer, offset, bytesToXor, _counterOutOffset);
+ _counterOutOffset += bytesToXor;
+ return bytesToXor;
}
- private void XorInPlace(Span buffer, int offset, int count)
+ private void XorInPlace(Span buffer, int offset, int count, int counterOffset)
{
for (var i = 0; i < count; i++)
{
- buffer[offset + i] = (byte)(_counterOut[i] ^ buffer[offset + i]);
+ buffer[offset + i] = (byte)(_counterOut[counterOffset + i] ^ buffer[offset + i]);
}
}
#endif
diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs
index 2ea1d2d7c..7d9cb753f 100644
--- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs
+++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs
@@ -16,7 +16,7 @@ internal partial class WinzipAesCryptoStream : Stream
private readonly ICryptoTransform _transform;
private int _nonce = 1;
private byte[] _counterOut = new byte[BLOCK_SIZE_IN_BYTES];
- private bool _isFinalBlock;
+ private int _counterOutOffset = BLOCK_SIZE_IN_BYTES;
private long _totalBytesLeftToRead;
private bool _isDisposed;
@@ -123,58 +123,45 @@ public override int Read(byte[] buffer, int offset, int count)
return read;
}
- private int ReadTransformOneBlock(byte[] buffer, int offset, int last)
+ private void FillCounterOut()
{
- if (_isFinalBlock)
- {
- throw new ArchiveOperationException();
- }
-
- var bytesRemaining = last - offset;
- var bytesToRead =
- (bytesRemaining > BLOCK_SIZE_IN_BYTES) ? BLOCK_SIZE_IN_BYTES : bytesRemaining;
-
// update the counter
BinaryPrimitives.WriteInt32LittleEndian(_counter, _nonce++);
-
- // Determine if this is the final block
- if ((bytesToRead == bytesRemaining) && (_totalBytesLeftToRead == 0))
- {
- _counterOut = _transform.TransformFinalBlock(_counter, 0, BLOCK_SIZE_IN_BYTES);
- _isFinalBlock = true;
- }
- else
- {
- _transform.TransformBlock(
- _counter,
- 0, // offset
- BLOCK_SIZE_IN_BYTES,
- _counterOut,
- 0
- ); // offset
- }
-
- XorInPlace(buffer, offset, bytesToRead);
- return bytesToRead;
+ _transform.TransformBlock(
+ _counter,
+ 0, // offset
+ BLOCK_SIZE_IN_BYTES,
+ _counterOut,
+ 0
+ ); // offset
+ _counterOutOffset = 0;
}
- private void XorInPlace(byte[] buffer, int offset, int count)
+ private void XorInPlace(byte[] buffer, int offset, int count, int counterOffset)
{
for (var i = 0; i < count; i++)
{
- buffer[offset + i] = (byte)(_counterOut[i] ^ buffer[offset + i]);
+ buffer[offset + i] = (byte)(_counterOut[counterOffset + i] ^ buffer[offset + i]);
}
}
private void ReadTransformBlocks(byte[] buffer, int offset, int count)
{
var posn = offset;
- var last = count + offset;
+ var remaining = count;
- while (posn < buffer.Length && posn < last)
+ while (posn < buffer.Length && remaining > 0)
{
- var n = ReadTransformOneBlock(buffer, posn, last);
- posn += n;
+ if (_counterOutOffset == BLOCK_SIZE_IN_BYTES)
+ {
+ FillCounterOut();
+ }
+
+ var bytesToXor = Math.Min(BLOCK_SIZE_IN_BYTES - _counterOutOffset, remaining);
+ XorInPlace(buffer, posn, bytesToXor, _counterOutOffset);
+ _counterOutOffset += bytesToXor;
+ posn += bytesToXor;
+ remaining -= bytesToXor;
}
}
diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs b/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs
index 3c60e7095..29f523a6f 100644
--- a/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs
+++ b/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs
@@ -48,10 +48,8 @@ protected async ValueTask GetCryptoStreamAsync(
}
if (
- (
- Header.CompressedSize == 0
- && FlagUtility.HasFlag(Header.Flags, HeaderFlags.UsePostDataDescriptor)
- ) || Header.IsZip64
+ Header.CompressedSize == 0
+ && FlagUtility.HasFlag(Header.Flags, HeaderFlags.UsePostDataDescriptor)
)
{
plainStream = SharpCompressStream.CreateNonDisposing(plainStream); //make sure AES doesn't close
diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.cs b/src/SharpCompress/Common/Zip/ZipFilePart.cs
index e4ac519c4..de182f377 100644
--- a/src/SharpCompress/Common/Zip/ZipFilePart.cs
+++ b/src/SharpCompress/Common/Zip/ZipFilePart.cs
@@ -234,10 +234,8 @@ protected Stream GetCryptoStream(Stream plainStream)
}
if (
- (
- Header.CompressedSize == 0
- && FlagUtility.HasFlag(Header.Flags, HeaderFlags.UsePostDataDescriptor)
- ) || Header.IsZip64
+ Header.CompressedSize == 0
+ && FlagUtility.HasFlag(Header.Flags, HeaderFlags.UsePostDataDescriptor)
)
{
plainStream = SharpCompressStream.CreateNonDisposing(plainStream); //make sure AES doesn't close
diff --git a/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs b/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs
new file mode 100644
index 000000000..578f6212f
--- /dev/null
+++ b/tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs
@@ -0,0 +1,257 @@
+using System;
+using System.Buffers.Binary;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Threading.Tasks;
+using SharpCompress.Common.Zip;
+using Xunit;
+
+namespace SharpCompress.Test.Streams;
+
+public class WinzipAesCryptoStreamTests
+{
+ [Fact]
+ public void Read_Decrypts_Data_For_Aligned_Buffer_Size()
+ {
+ const string password = "sample-password";
+ byte[] plainText = Enumerable.Range(0, 64).Select(i => (byte)i).ToArray();
+ byte[] salt = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87];
+ using var stream = CreateStream(plainText, password, salt);
+
+ byte[] actual = new byte[plainText.Length];
+ int bytesRead = stream.Read(actual, 0, actual.Length);
+
+ Assert.Equal(plainText.Length, bytesRead);
+ Assert.Equal(plainText, actual);
+ }
+
+ [Fact]
+ public void Read_Preserves_Keystream_Between_NonAligned_Reads()
+ {
+ const string password = "sample-password";
+ byte[] plainText = Enumerable.Range(0, 97).Select(i => (byte)i).ToArray();
+ byte[] salt = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87];
+ using var stream = CreateStream(plainText, password, salt);
+
+ byte[] actual = ReadWithChunkPattern(
+ (buffer, offset, count) => stream.Read(buffer, offset, count),
+ plainText.Length,
+ [13, 5, 29, 7, 43]
+ );
+
+ Assert.Equal(plainText, actual);
+ }
+
+ [Fact]
+ public async Task ReadAsync_Preserves_Keystream_Between_NonAligned_Reads()
+ {
+ const string password = "sample-password";
+ byte[] plainText = Enumerable
+ .Range(0, 113)
+ .Select(i => unchecked((byte)(255 - i)))
+ .ToArray();
+ byte[] salt = [0x91, 0x82, 0x73, 0x64, 0x55, 0x46, 0x37, 0x28];
+ using var stream = CreateStream(plainText, password, salt);
+
+ byte[] actual = await ReadWithChunkPatternAsync(
+ (buffer, offset, count) => stream.ReadAsync(buffer, offset, count),
+ plainText.Length,
+ [11, 3, 17, 5, 41]
+ );
+
+ Assert.Equal(plainText, actual);
+ }
+
+ [Fact]
+ public async Task ReadAsync_Memory_Preserves_Keystream_Between_NonAligned_Reads()
+ {
+ const string password = "sample-password";
+ byte[] plainText = Enumerable
+ .Range(0, 113)
+ .Select(i => unchecked((byte)(255 - i)))
+ .ToArray();
+ byte[] salt = [0x91, 0x82, 0x73, 0x64, 0x55, 0x46, 0x37, 0x28];
+ using var stream = CreateStream(plainText, password, salt);
+
+ byte[] actual = await ReadWithChunkPatternMemoryAsync(
+ stream,
+ plainText.Length,
+ [11, 3, 17, 5, 41]
+ );
+
+ Assert.Equal(plainText, actual);
+ }
+
+ [Fact]
+ public void Read_Stops_At_Encrypted_Payload_Length()
+ {
+ const string password = "sample-password";
+ byte[] plainText = Enumerable.Range(0, 31).Select(i => (byte)(i * 3)).ToArray();
+ byte[] salt = [0xA1, 0xB2, 0xC3, 0xD4, 0x01, 0x12, 0x23, 0x34];
+ using var stream = CreateStream(plainText, password, salt);
+
+ byte[] actual = new byte[plainText.Length + 16];
+ int bytesRead = stream.Read(actual, 0, actual.Length);
+ int eofRead = stream.Read(actual, bytesRead, actual.Length - bytesRead);
+
+ Assert.Equal(plainText.Length, bytesRead);
+ Assert.Equal(0, eofRead);
+ Assert.Equal(plainText, actual.Take(bytesRead).ToArray());
+ }
+
+ private static WinzipAesCryptoStream CreateStream(
+ byte[] plainText,
+ string password,
+ byte[] salt
+ )
+ {
+ var encryptionData = CreateEncryptionData(password, salt);
+ byte[] cipherText = EncryptCtr(plainText, encryptionData.KeyBytes);
+ byte[] archiveBytes = cipherText.Concat(new byte[10]).ToArray();
+ return new WinzipAesCryptoStream(
+ new MemoryStream(archiveBytes, writable: false),
+ encryptionData,
+ cipherText.Length
+ );
+ }
+
+ [SuppressMessage(
+ "Security",
+ "CA5379:Rfc2898DeriveBytes might be using a weak hash algorithm",
+ Justification = "WinZip AES interop requires PBKDF2 with SHA-1."
+ )]
+ private static WinzipAesEncryptionData CreateEncryptionData(string password, byte[] salt)
+ {
+#pragma warning disable SYSLIB0060 // Rfc2898DeriveBytes might be using a weak hash algorithm
+ using var deriveBytes = new Rfc2898DeriveBytes(
+ password,
+ salt,
+ 1000,
+ HashAlgorithmName.SHA1
+ );
+#pragma warning restore SYSLIB0060
+ deriveBytes.GetBytes(16);
+ deriveBytes.GetBytes(16);
+ byte[] passwordVerifyValue = deriveBytes.GetBytes(2);
+
+ return new WinzipAesEncryptionData(
+ WinzipAesKeySize.KeySize128,
+ salt,
+ passwordVerifyValue,
+ password
+ );
+ }
+
+ private static byte[] EncryptCtr(byte[] plainText, byte[] keyBytes)
+ {
+ using var aes = Aes.Create();
+ aes.BlockSize = 128;
+ aes.KeySize = keyBytes.Length * 8;
+ aes.Mode = CipherMode.ECB;
+ aes.Padding = PaddingMode.None;
+
+ using var encryptor = aes.CreateEncryptor(keyBytes, new byte[16]);
+ byte[] counter = new byte[16];
+ byte[] counterOut = new byte[16];
+ byte[] cipherText = new byte[plainText.Length];
+ int nonce = 1;
+ int offset = 0;
+
+ while (offset < plainText.Length)
+ {
+ BinaryPrimitives.WriteInt32LittleEndian(counter, nonce++);
+ encryptor.TransformBlock(counter, 0, counter.Length, counterOut, 0);
+
+ int blockLength = Math.Min(counterOut.Length, plainText.Length - offset);
+ for (int i = 0; i < blockLength; i++)
+ {
+ cipherText[offset + i] = (byte)(plainText[offset + i] ^ counterOut[i]);
+ }
+
+ offset += blockLength;
+ }
+
+ return cipherText;
+ }
+
+ private static byte[] ReadWithChunkPattern(
+ Func read,
+ int totalLength,
+ int[] chunkPattern
+ )
+ {
+ byte[] actual = new byte[totalLength];
+ int offset = 0;
+ int chunkIndex = 0;
+
+ while (offset < totalLength)
+ {
+ int requested = Math.Min(
+ chunkPattern[chunkIndex % chunkPattern.Length],
+ totalLength - offset
+ );
+ int bytesRead = read(actual, offset, requested);
+ Assert.True(bytesRead > 0);
+ offset += bytesRead;
+ chunkIndex++;
+ }
+
+ return actual;
+ }
+
+ private static async Task ReadWithChunkPatternAsync(
+ Func> readAsync,
+ int totalLength,
+ int[] chunkPattern
+ )
+ {
+ byte[] actual = new byte[totalLength];
+ int offset = 0;
+ int chunkIndex = 0;
+
+ while (offset < totalLength)
+ {
+ int requested = Math.Min(
+ chunkPattern[chunkIndex % chunkPattern.Length],
+ totalLength - offset
+ );
+ int bytesRead = await readAsync(actual, offset, requested);
+ Assert.True(bytesRead > 0);
+ offset += bytesRead;
+ chunkIndex++;
+ }
+
+ return actual;
+ }
+
+ private static async Task ReadWithChunkPatternMemoryAsync(
+ Stream stream,
+ int totalLength,
+ int[] chunkPattern
+ )
+ {
+ byte[] actual = new byte[totalLength];
+ int offset = 0;
+ int chunkIndex = 0;
+
+ while (offset < totalLength)
+ {
+ int requested = Math.Min(
+ chunkPattern[chunkIndex % chunkPattern.Length],
+ totalLength - offset
+ );
+#if NET48
+ int bytesRead = await stream.ReadAsync(actual, offset, requested);
+#else
+ int bytesRead = await stream.ReadAsync(actual.AsMemory(offset, requested));
+#endif
+ Assert.True(bytesRead > 0);
+ offset += bytesRead;
+ chunkIndex++;
+ }
+
+ return actual;
+ }
+}
diff --git a/tests/SharpCompress.Test/TestBase.cs b/tests/SharpCompress.Test/TestBase.cs
index 449c8f0f0..1b4656d6a 100644
--- a/tests/SharpCompress.Test/TestBase.cs
+++ b/tests/SharpCompress.Test/TestBase.cs
@@ -16,10 +16,6 @@ public class TestBase : IAsyncDisposable
public static readonly string TEST_ARCHIVES_PATH;
public static readonly string ORIGINAL_FILES_PATH;
public static readonly string MISC_TEST_FILES_PATH;
- private static readonly string SCRATCH_BASE_PATH;
-
- private static readonly string SCRATCH_DIRECTORY;
- private static readonly string SCRATCH2_DIRECTORY;
static TestBase()
{
@@ -33,23 +29,18 @@ static TestBase()
TEST_ARCHIVES_PATH = Path.Combine(SOLUTION_BASE_PATH, "TestArchives", "Archives");
ORIGINAL_FILES_PATH = Path.Combine(SOLUTION_BASE_PATH, "TestArchives", "Original");
MISC_TEST_FILES_PATH = Path.Combine(SOLUTION_BASE_PATH, "TestArchives", "MiscTest");
-
- SCRATCH_BASE_PATH = Path.Combine(SOLUTION_BASE_PATH, "TestArchives");
- SCRATCH_DIRECTORY = Path.Combine(SCRATCH_BASE_PATH, "Scratch");
- SCRATCH2_DIRECTORY = Path.Combine(SCRATCH_BASE_PATH, "Scratch2");
-
- Directory.CreateDirectory(SCRATCH_DIRECTORY);
- Directory.CreateDirectory(SCRATCH2_DIRECTORY);
}
private readonly Guid _testGuid = Guid.NewGuid();
+ private readonly string _testTempDirectory;
protected readonly string SCRATCH_FILES_PATH;
protected readonly string SCRATCH2_FILES_PATH;
protected TestBase()
{
- SCRATCH_FILES_PATH = Path.Combine(SCRATCH_DIRECTORY, _testGuid.ToString());
- SCRATCH2_FILES_PATH = Path.Combine(SCRATCH2_DIRECTORY, _testGuid.ToString());
+ _testTempDirectory = Path.Combine(Path.GetTempPath(), $"SharpCompress.Test.{_testGuid:N}");
+ SCRATCH_FILES_PATH = Path.Combine(_testTempDirectory, "Scratch");
+ SCRATCH2_FILES_PATH = Path.Combine(_testTempDirectory, "Scratch2");
Directory.CreateDirectory(SCRATCH_FILES_PATH);
Directory.CreateDirectory(SCRATCH2_FILES_PATH);
@@ -59,22 +50,50 @@ protected TestBase()
public async ValueTask DisposeAsync()
{
await Task.CompletedTask;
- Directory.Delete(SCRATCH_FILES_PATH, true);
- Directory.Delete(SCRATCH2_FILES_PATH, true);
+ DeleteScratchDirectory(_testTempDirectory);
}
public void CleanScratch()
{
- if (Directory.Exists(SCRATCH_FILES_PATH))
+ ResetScratchDirectory(SCRATCH_FILES_PATH);
+ ResetScratchDirectory(SCRATCH2_FILES_PATH);
+ }
+
+ private static void ResetScratchDirectory(string path)
+ {
+ if (Directory.Exists(path))
{
- Directory.Delete(SCRATCH_FILES_PATH, true);
+ Directory.Delete(path, true);
}
- Directory.CreateDirectory(SCRATCH_FILES_PATH);
- if (Directory.Exists(SCRATCH2_FILES_PATH))
+
+ Directory.CreateDirectory(path);
+ }
+
+ private static void DeleteScratchDirectory(string path)
+ {
+ if (!Directory.Exists(path))
{
- Directory.Delete(SCRATCH2_FILES_PATH, true);
+ return;
+ }
+
+ try
+ {
+ Directory.Delete(path, true);
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ throw new InvalidOperationException(
+ $"Failed to clean up temp test directory '{path}'.",
+ ex
+ );
+ }
+
+ if (Directory.Exists(path))
+ {
+ throw new InvalidOperationException(
+ $"Temp test directory '{path}' was not cleaned up."
+ );
}
- Directory.CreateDirectory(SCRATCH2_FILES_PATH);
}
public void VerifyFiles()
diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs
index 914688c2b..92073da41 100644
--- a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs
+++ b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs
@@ -15,6 +15,8 @@ namespace SharpCompress.Test.Zip;
public class ZipArchiveTests : ArchiveTests
{
+ private const long GeneratedZip64EntrySize = (long)uint.MaxValue + 1;
+
public ZipArchiveTests() => UseExtensionInsteadOfNameToVerify = true;
[Fact]
@@ -516,6 +518,53 @@ public void Zip_WinzipAES_CompressionType()
}
}
+ [Fact]
+ public void Zip_Zstandard_WinzipAES_Mixed_ArchiveFileRead()
+ {
+ using var archive = ZipArchive.OpenArchive(
+ Path.Combine(TEST_ARCHIVES_PATH, "Zip.zstd.WinzipAES.mixed.zip"),
+ new ReaderOptions { Password = "test" }
+ );
+
+ VerifyMixedZstandardArchive(archive);
+ }
+
+ [Fact]
+ public void Zip_Zstandard_WinzipAES_Mixed_ArchiveStreamRead()
+ {
+ using var stream = File.OpenRead(
+ Path.Combine(TEST_ARCHIVES_PATH, "Zip.zstd.WinzipAES.mixed.zip")
+ );
+ using var archive = ZipArchive.OpenArchive(stream, new ReaderOptions { Password = "test" });
+
+ VerifyMixedZstandardArchive(archive);
+ }
+
+ [Fact(Explicit = true)]
+ [Trait("zip64", "generated")]
+ public void Zip_Zip64_GeneratedArchive_StreamIsBoundedToEntryLength()
+ {
+ var zipPath = Path.Combine(SCRATCH2_FILES_PATH, "generated.zip64.large.zip");
+ CreateZip64Archive(zipPath);
+
+ using var archive = ZipArchive.OpenArchive(zipPath);
+ var entries = archive
+ .Entries.Where(x => !x.IsDirectory)
+ .OrderByDescending(x => x.Size)
+ .ToArray();
+
+ Assert.Equal(2, entries.Length);
+ Assert.Equal(GeneratedZip64EntrySize, entries[0].Size);
+ Assert.Equal(1, entries[1].Size);
+
+ using var firstStream = entries[0].OpenEntryStream();
+ Assert.Equal(entries[0].Size, CountBytes(firstStream));
+
+ using var secondStream = entries[1].OpenEntryStream();
+ Assert.Equal(0x42, secondStream.ReadByte());
+ Assert.Equal(-1, secondStream.ReadByte());
+ }
+
[Fact]
public void Zip_Pkware_CompressionType()
{
@@ -910,4 +959,89 @@ public void Zip_LZMA_ZeroSizeEntry_CanExtract()
entries[0].WriteTo(outStream);
Assert.Equal(0, outStream.Length);
}
+
+ private static void VerifyMixedZstandardArchive(IArchive archive)
+ {
+ var entries = archive.Entries.Where(x => !x.IsDirectory).ToArray();
+ Assert.Equal(4, entries.Length);
+ Assert.Equal(2, entries.Count(x => x.IsEncrypted));
+ Assert.Equal(
+ [".signature", "encrypted-zstd-entry.bin", "plain-zstd-entry.bin", "tables.db"],
+ entries.Select(x => x.Key.NotNull()).OrderBy(x => x).ToArray()
+ );
+ Assert.All(
+ entries,
+ entry => Assert.Equal(CompressionType.ZStandard, entry.CompressionType)
+ );
+
+ var expectedSizes = new long[] { 160, 64 * 1024, 64 * 1024, 192 * 1024 };
+ Assert.Equal(expectedSizes, entries.Select(x => x.Size).OrderBy(x => x).ToArray());
+
+ foreach (var entry in entries)
+ {
+ using var entryStream = entry.OpenEntryStream();
+ using var target = new MemoryStream();
+ entryStream.CopyTo(target);
+ Assert.Equal(entry.Size, target.Length);
+ }
+ }
+
+ private static long CountBytes(Stream stream)
+ {
+ var buffer = new byte[8 * 1024 * 1024];
+ long total = 0;
+ int read;
+ while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ total += read;
+ }
+
+ return total;
+ }
+
+ private static void CreateZip64Archive(string path)
+ {
+ if (File.Exists(path))
+ {
+ File.Delete(path);
+ }
+
+ var writerOptions = new ZipWriterOptions(CompressionType.None) { UseZip64 = true };
+ using var fileStream = File.OpenWrite(path);
+ using var zipWriter = (ZipWriter)
+ WriterFactory.OpenWriter(fileStream, ArchiveType.Zip, writerOptions);
+
+ using (
+ var largeEntryStream = zipWriter.WriteToStream(
+ "large-entry.bin",
+ new ZipWriterEntryOptions()
+ )
+ )
+ {
+ WriteZeroes(largeEntryStream, GeneratedZip64EntrySize);
+ }
+
+ using (
+ var trailingEntryStream = zipWriter.WriteToStream(
+ "trailing-entry.bin",
+ new ZipWriterEntryOptions()
+ )
+ )
+ {
+ trailingEntryStream.WriteByte(0x42);
+ }
+ }
+
+ private static void WriteZeroes(Stream stream, long length)
+ {
+ byte[] buffer = new byte[8 * 1024 * 1024];
+ long remaining = length;
+
+ while (remaining > 0)
+ {
+ int chunk = (int)Math.Min(buffer.Length, remaining);
+ stream.Write(buffer, 0, chunk);
+ remaining -= chunk;
+ }
+ }
}
diff --git a/tests/SharpCompress.Test/Zip/ZipFilePartTests.cs b/tests/SharpCompress.Test/Zip/ZipFilePartTests.cs
new file mode 100644
index 000000000..a299cfbf7
--- /dev/null
+++ b/tests/SharpCompress.Test/Zip/ZipFilePartTests.cs
@@ -0,0 +1,59 @@
+using System.IO;
+using SharpCompress.Common;
+using SharpCompress.Common.Zip;
+using SharpCompress.Common.Zip.Headers;
+using SharpCompress.IO;
+using SharpCompress.Providers;
+using Xunit;
+
+namespace SharpCompress.Test.Zip;
+
+public class ZipFilePartTests
+{
+ [Fact]
+ public void GetCryptoStream_Bounds_Known_Size_Zip64_Entries()
+ {
+ var header = new DirectoryEntryHeader(new ArchiveEncoding())
+ {
+ Name = "entry.bin",
+ CompressionMethod = ZipCompressionMethod.None,
+ CompressedSize = uint.MaxValue,
+ UncompressedSize = uint.MaxValue,
+ };
+
+ using var backingStream = new MemoryStream([1, 2, 3, 4, 5], writable: false);
+ var part = new TestZipFilePart(header, backingStream);
+
+ using var cryptoStream = part.OpenCryptoStream();
+
+ Assert.IsType(cryptoStream);
+ }
+
+ [Fact]
+ public void GetCryptoStream_Leaves_DataDescriptor_Entries_Unbounded_When_Size_Is_Unknown()
+ {
+ var header = new DirectoryEntryHeader(new ArchiveEncoding())
+ {
+ Name = "entry.bin",
+ CompressionMethod = ZipCompressionMethod.None,
+ CompressedSize = 0,
+ UncompressedSize = 0,
+ Flags = HeaderFlags.UsePostDataDescriptor,
+ };
+
+ using var backingStream = new MemoryStream([1, 2, 3, 4, 5], writable: false);
+ var part = new TestZipFilePart(header, backingStream);
+
+ using var cryptoStream = part.OpenCryptoStream();
+
+ Assert.IsNotType(cryptoStream);
+ }
+
+ private sealed class TestZipFilePart(ZipFileEntry header, Stream stream)
+ : ZipFilePart(header, stream, CompressionProviderRegistry.Default)
+ {
+ public Stream OpenCryptoStream() => GetCryptoStream(CreateBaseStream());
+
+ protected override Stream CreateBaseStream() => BaseStream;
+ }
+}
diff --git a/tests/TestArchives/Archives/Zip.zstd.WinzipAES.mixed.zip b/tests/TestArchives/Archives/Zip.zstd.WinzipAES.mixed.zip
new file mode 100644
index 000000000..506e49c9b
Binary files /dev/null and b/tests/TestArchives/Archives/Zip.zstd.WinzipAES.mixed.zip differ