mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-04-05 21:51:09 +00:00
Merge pull request #1253 from adamhathcock/adam/fix-zip645-and-zipzstd
Fix ZIP64 stream bounding and WinZip AES read-state corruption in ZIP reader
This commit is contained in:
@@ -11,5 +11,6 @@
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||
<NoWarn>${NoWarn};IDE0051</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -86,55 +86,34 @@ internal partial class WinzipAesCryptoStream
|
||||
private void ReadTransformBlocks(Span<byte> 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<byte> buffer, int offset, int last)
|
||||
private int ReadTransformOneBlock(Span<byte> 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<byte> buffer, int offset, int count)
|
||||
private void XorInPlace(Span<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]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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 @@ internal partial class WinzipAesCryptoStream : Stream
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,10 +48,8 @@ internal abstract partial class ZipFilePart
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -234,10 +234,8 @@ internal abstract partial class ZipFilePart : FilePart
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
257
tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs
Normal file
257
tests/SharpCompress.Test/Streams/WinzipAesCryptoStreamTests.cs
Normal file
@@ -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<byte[], int, int, int> 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<byte[]> ReadWithChunkPatternAsync(
|
||||
Func<byte[], int, int, Task<int>> 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<byte[]> 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;
|
||||
}
|
||||
}
|
||||
@@ -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 @@ public class TestBase : IAsyncDisposable
|
||||
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 @@ public class TestBase : IAsyncDisposable
|
||||
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()
|
||||
|
||||
@@ -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 class ZipArchiveTests : ArchiveTests
|
||||
}
|
||||
}
|
||||
|
||||
[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 class ZipArchiveTests : ArchiveTests
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
59
tests/SharpCompress.Test/Zip/ZipFilePartTests.cs
Normal file
59
tests/SharpCompress.Test/Zip/ZipFilePartTests.cs
Normal file
@@ -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<ReadOnlySubStream>(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<ReadOnlySubStream>(cryptoStream);
|
||||
}
|
||||
|
||||
private sealed class TestZipFilePart(ZipFileEntry header, Stream stream)
|
||||
: ZipFilePart(header, stream, CompressionProviderRegistry.Default)
|
||||
{
|
||||
public Stream OpenCryptoStream() => GetCryptoStream(CreateBaseStream());
|
||||
|
||||
protected override Stream CreateBaseStream() => BaseStream;
|
||||
}
|
||||
}
|
||||
BIN
tests/TestArchives/Archives/Zip.zstd.WinzipAES.mixed.zip
Normal file
BIN
tests/TestArchives/Archives/Zip.zstd.WinzipAES.mixed.zip
Normal file
Binary file not shown.
Reference in New Issue
Block a user