mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-11-15 17:02:03 +00:00
Merge pull request #1452 from romaklayt/fix/zip-encrypted-data-descriptor
Read encrypted zip entries written with a data descriptor
This commit is contained in:
@@ -138,15 +138,16 @@ internal sealed partial class SeekableZipHeaderFactory
|
||||
using var reader = new AsyncBinaryReader(stream, leaveOpen: true);
|
||||
#endif
|
||||
var signature = await reader.ReadUInt32Async().ConfigureAwait(false);
|
||||
if (
|
||||
await ReadHeader(signature, reader, _zip64).ConfigureAwait(false)
|
||||
is not LocalEntryHeader localEntryHeader
|
||||
)
|
||||
if (signature != ENTRY_HEADER_BYTES)
|
||||
{
|
||||
throw new ArchiveOperationException();
|
||||
}
|
||||
|
||||
// Sizes written to a data descriptor are only known from the central directory and are
|
||||
// needed before the encryption header is loaded.
|
||||
var localEntryHeader = await ReadLocalHeader(reader).ConfigureAwait(false);
|
||||
PopulateDirectoryEntryMetadata(localEntryHeader, directoryEntryHeader);
|
||||
await LoadHeaderAsync(localEntryHeader, stream).ConfigureAwait(false);
|
||||
return localEntryHeader;
|
||||
}
|
||||
|
||||
|
||||
@@ -146,12 +146,16 @@ internal sealed partial class SeekableZipHeaderFactory : ZipHeaderFactory
|
||||
stream.Seek(directoryEntryHeader.RelativeOffsetOfEntryHeader, SeekOrigin.Begin);
|
||||
var reader = new BinaryReader(stream);
|
||||
var signature = reader.ReadUInt32();
|
||||
if (ReadHeader(signature, reader, _zip64) is not LocalEntryHeader localEntryHeader)
|
||||
if (signature != ENTRY_HEADER_BYTES)
|
||||
{
|
||||
throw new ArchiveOperationException();
|
||||
}
|
||||
|
||||
// Sizes written to a data descriptor are only known from the central directory and are
|
||||
// needed before the encryption header is loaded.
|
||||
var localEntryHeader = ReadLocalHeader(reader);
|
||||
PopulateDirectoryEntryMetadata(localEntryHeader, directoryEntryHeader);
|
||||
LoadHeader(localEntryHeader, stream);
|
||||
return localEntryHeader;
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,8 @@ internal sealed partial class StreamingZipHeaderFactory
|
||||
continue;
|
||||
}
|
||||
|
||||
_headerFactory.SeekPastResolvedEntry(lastEntryHeader, _sharpCompressStream);
|
||||
|
||||
var pos = _sharpCompressStream.CanSeek
|
||||
? (long?)_sharpCompressStream.Position
|
||||
: null;
|
||||
@@ -301,8 +303,12 @@ internal sealed partial class StreamingZipHeaderFactory
|
||||
localHeader.IsCrcAvailable = true;
|
||||
}
|
||||
|
||||
if (_headerFactory.IsResolvedDirectory(localHeader))
|
||||
{
|
||||
header.HasData = false;
|
||||
}
|
||||
// If we have CompressedSize, there is data to be read
|
||||
if (localHeader.CompressedSize > 0)
|
||||
else if (localHeader.CompressedSize > 0)
|
||||
{
|
||||
header.HasData = true;
|
||||
} // Check if zip is streaming ( Length is 0 and is declared in PostDataDescriptor )
|
||||
@@ -317,6 +323,7 @@ internal sealed partial class StreamingZipHeaderFactory
|
||||
|
||||
// Check if next data is PostDataDescriptor, streamed file with 0 length
|
||||
header.HasData = !IsHeader(nextHeaderBytes);
|
||||
ThrowIfUnreadableEncryptedDirectory(localHeader, header.HasData);
|
||||
}
|
||||
else // We are not streaming and compressed size is 0, we have no data
|
||||
{
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Zip.Headers;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Common.Zip;
|
||||
|
||||
/// <summary>
|
||||
/// Encrypted entries written with a post data descriptor have no sizes in their local header, and the
|
||||
/// descriptor cannot be found by scanning encrypted data. When the archive stream is seekable, the sizes
|
||||
/// are taken from the central directory, walked by a forward-only cursor so memory use stays constant
|
||||
/// regardless of the number of entries.
|
||||
/// </summary>
|
||||
internal sealed partial class StreamingZipHeaderFactory
|
||||
{
|
||||
private const string EncryptedDataDescriptorNotSupported =
|
||||
"SharpCompress cannot currently read non-seekable Zip Streams with encrypted data that has been written in a non-seekable manner.";
|
||||
|
||||
private IEnumerator<ZipHeader>? _centralDirectory;
|
||||
private IAsyncEnumerator<ZipHeader>? _centralDirectoryAsync;
|
||||
private bool _centralDirectoryUnavailable;
|
||||
private bool _centralDirectoryExhausted;
|
||||
private LocalEntryHeader? _resolvedEntry;
|
||||
private long _resolvedEntryDataEnd;
|
||||
|
||||
protected override bool TryResolveDescriptorSizes(ZipFileEntry entryHeader, Stream stream)
|
||||
{
|
||||
var seekableStream = GetSeekableStream(stream);
|
||||
if (
|
||||
_centralDirectoryUnavailable
|
||||
|| seekableStream is null
|
||||
|| entryHeader is not LocalEntryHeader localHeader
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var dataStart = seekableStream.Position;
|
||||
try
|
||||
{
|
||||
var directoryEntry = FindDirectoryEntry(seekableStream, localHeader);
|
||||
return directoryEntry is not null && Resolve(localHeader, directoryEntry, dataStart);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
// An unreadable central directory leaves the forward-only behavior in place.
|
||||
_centralDirectoryUnavailable = true;
|
||||
ResetCentralDirectory();
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
seekableStream.Position = dataStart;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async ValueTask<bool> TryResolveDescriptorSizesAsync(
|
||||
ZipFileEntry entryHeader,
|
||||
Stream stream
|
||||
)
|
||||
{
|
||||
var seekableStream = GetSeekableStream(stream);
|
||||
if (
|
||||
_centralDirectoryUnavailable
|
||||
|| seekableStream is null
|
||||
|| entryHeader is not LocalEntryHeader localHeader
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var dataStart = seekableStream.Position;
|
||||
try
|
||||
{
|
||||
var directoryEntry = await FindDirectoryEntryAsync(seekableStream, localHeader)
|
||||
.ConfigureAwait(false);
|
||||
return directoryEntry is not null && Resolve(localHeader, directoryEntry, dataStart);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
// An unreadable central directory leaves the forward-only behavior in place.
|
||||
_centralDirectoryUnavailable = true;
|
||||
await ResetCentralDirectoryAsync().ConfigureAwait(false);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
seekableStream.Position = dataStart;
|
||||
}
|
||||
}
|
||||
|
||||
private DirectoryEntryHeader? FindDirectoryEntry(Stream stream, LocalEntryHeader localHeader)
|
||||
{
|
||||
// The central directory normally lists entries in local header order, so the cursor only
|
||||
// moves forward; a single restart covers archives that order it differently.
|
||||
for (var pass = 0; pass < 2; pass++)
|
||||
{
|
||||
_centralDirectory ??= new SeekableZipHeaderFactory(null, ArchiveEncoding)
|
||||
.ReadSeekableHeader(stream)
|
||||
.GetEnumerator();
|
||||
while (!_centralDirectoryExhausted && _centralDirectory.MoveNext())
|
||||
{
|
||||
if (IsMatch(_centralDirectory.Current, localHeader) is { } directoryEntry)
|
||||
{
|
||||
return directoryEntry;
|
||||
}
|
||||
}
|
||||
|
||||
ResetCentralDirectory();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async ValueTask<DirectoryEntryHeader?> FindDirectoryEntryAsync(
|
||||
Stream stream,
|
||||
LocalEntryHeader localHeader
|
||||
)
|
||||
{
|
||||
// The central directory normally lists entries in local header order, so the cursor only
|
||||
// moves forward; a single restart covers archives that order it differently.
|
||||
for (var pass = 0; pass < 2; pass++)
|
||||
{
|
||||
_centralDirectoryAsync ??= new SeekableZipHeaderFactory(null, ArchiveEncoding)
|
||||
.ReadSeekableHeaderAsync(stream)
|
||||
.GetAsyncEnumerator();
|
||||
while (
|
||||
!_centralDirectoryExhausted
|
||||
&& await _centralDirectoryAsync.MoveNextAsync().ConfigureAwait(false)
|
||||
)
|
||||
{
|
||||
if (IsMatch(_centralDirectoryAsync.Current, localHeader) is { } directoryEntry)
|
||||
{
|
||||
return directoryEntry;
|
||||
}
|
||||
}
|
||||
|
||||
await ResetCentralDirectoryAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private DirectoryEntryHeader? IsMatch(ZipHeader header, LocalEntryHeader localHeader)
|
||||
{
|
||||
switch (header)
|
||||
{
|
||||
case DirectoryEntryHeader directoryEntry
|
||||
when string.Equals(directoryEntry.Name, localHeader.Name, StringComparison.Ordinal):
|
||||
return directoryEntry;
|
||||
case DirectoryEndHeader:
|
||||
// The central directory enumerator does not stop at the end record.
|
||||
_centralDirectoryExhausted = true;
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool Resolve(
|
||||
LocalEntryHeader localHeader,
|
||||
DirectoryEntryHeader directoryEntry,
|
||||
long dataStart
|
||||
)
|
||||
{
|
||||
localHeader.CompressedSize = directoryEntry.CompressedSize;
|
||||
localHeader.UncompressedSize = directoryEntry.UncompressedSize;
|
||||
localHeader.Crc = directoryEntry.Crc;
|
||||
localHeader.IsCrcAvailable = true;
|
||||
_resolvedEntry = localHeader;
|
||||
_resolvedEntryDataEnd = dataStart + directoryEntry.CompressedSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ResetCentralDirectory()
|
||||
{
|
||||
_centralDirectory?.Dispose();
|
||||
_centralDirectory = null;
|
||||
_centralDirectoryExhausted = false;
|
||||
}
|
||||
|
||||
private async ValueTask ResetCentralDirectoryAsync()
|
||||
{
|
||||
if (_centralDirectoryAsync is not null)
|
||||
{
|
||||
await _centralDirectoryAsync.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
_centralDirectoryAsync = null;
|
||||
_centralDirectoryExhausted = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positions the stream at the data descriptor of an entry whose sizes came from the central
|
||||
/// directory; decoders may stop short of the entry end (e.g. the WinZip AES authentication code).
|
||||
/// </summary>
|
||||
private void SeekPastResolvedEntry(LocalEntryHeader lastEntryHeader, Stream stream)
|
||||
{
|
||||
if (ReferenceEquals(lastEntryHeader, _resolvedEntry))
|
||||
{
|
||||
stream.Position = _resolvedEntryDataEnd;
|
||||
_resolvedEntry = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An encrypted directory resolved from the central directory carries only its encryption header,
|
||||
/// which is skipped together with the data descriptor.
|
||||
/// </summary>
|
||||
private bool IsResolvedDirectory(LocalEntryHeader localHeader) =>
|
||||
localHeader.IsDirectory && ReferenceEquals(localHeader, _resolvedEntry);
|
||||
|
||||
/// <summary>
|
||||
/// Fails instead of silently ending the entry enumeration when an encrypted directory written with
|
||||
/// a data descriptor has payload of unknown size (the following entries could not be located).
|
||||
/// </summary>
|
||||
private static void ThrowIfUnreadableEncryptedDirectory(
|
||||
LocalEntryHeader localHeader,
|
||||
bool hasData
|
||||
)
|
||||
{
|
||||
if (
|
||||
hasData
|
||||
&& localHeader.IsDirectory
|
||||
&& FlagUtility.HasFlag(localHeader.Flags, HeaderFlags.Encrypted)
|
||||
)
|
||||
{
|
||||
throw new NotSupportedException(EncryptedDataDescriptorNotSupported);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the seekable stream underneath SharpCompress wrappers, or null when the archive can
|
||||
/// only be read forward (a buffered <see cref="SharpCompressStream"/> only seeks within its buffer).
|
||||
/// </summary>
|
||||
private static Stream? GetSeekableStream(Stream stream)
|
||||
{
|
||||
while (stream is SharpCompressStream { IsPassthrough: true } sharpCompressStream)
|
||||
{
|
||||
stream = sharpCompressStream.BaseStream();
|
||||
}
|
||||
|
||||
if (stream is SeekableSharpCompressStream seekableSharpCompressStream)
|
||||
{
|
||||
stream = seekableSharpCompressStream.BaseStream();
|
||||
}
|
||||
|
||||
return stream is SharpCompressStream ? null
|
||||
: stream.CanSeek ? stream
|
||||
: null;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Common.Zip;
|
||||
|
||||
internal partial class StreamingZipHeaderFactory : ZipHeaderFactory
|
||||
internal sealed partial class StreamingZipHeaderFactory : ZipHeaderFactory
|
||||
{
|
||||
private IEnumerable<ZipEntry>? _entries;
|
||||
|
||||
@@ -47,6 +47,7 @@ internal partial class StreamingZipHeaderFactory : ZipHeaderFactory
|
||||
}
|
||||
|
||||
// removed requirement for FixStreamedFileLocation()
|
||||
SeekPastResolvedEntry(_lastEntryHeader, sharpCompressStream);
|
||||
|
||||
var pos = sharpCompressStream.CanSeek
|
||||
? (long?)sharpCompressStream.Position
|
||||
@@ -209,8 +210,12 @@ internal partial class StreamingZipHeaderFactory : ZipHeaderFactory
|
||||
localHeader.IsCrcAvailable = true;
|
||||
}
|
||||
|
||||
if (IsResolvedDirectory(localHeader))
|
||||
{
|
||||
header.HasData = false;
|
||||
}
|
||||
// If we have CompressedSize, there is data to be read
|
||||
if (localHeader.CompressedSize > 0)
|
||||
else if (localHeader.CompressedSize > 0)
|
||||
{
|
||||
header.HasData = true;
|
||||
} // Check if zip is streaming ( Length is 0 and is declared in PostDataDescriptor )
|
||||
@@ -223,6 +228,7 @@ internal partial class StreamingZipHeaderFactory : ZipHeaderFactory
|
||||
|
||||
// Check if next data is PostDataDescriptor, streamed file with 0 length
|
||||
header.HasData = !IsHeader(nextHeaderBytes);
|
||||
ThrowIfUnreadableEncryptedDirectory(localHeader, header.HasData);
|
||||
}
|
||||
else // We are not streaming and compressed size is 0, we have no data
|
||||
{
|
||||
|
||||
@@ -93,13 +93,31 @@ internal partial class ZipHeaderFactory
|
||||
return entryHeader;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronous counterpart of <see cref="TryResolveDescriptorSizes"/>.
|
||||
/// </summary>
|
||||
protected virtual ValueTask<bool> TryResolveDescriptorSizesAsync(
|
||||
ZipFileEntry entryHeader,
|
||||
Stream stream
|
||||
) => new(false);
|
||||
|
||||
/// <summary>
|
||||
/// Loads encryption metadata and stream positioning for a header using async reads where needed.
|
||||
/// </summary>
|
||||
private async ValueTask LoadHeaderAsync(ZipFileEntry entryHeader, Stream stream)
|
||||
protected async ValueTask LoadHeaderAsync(ZipFileEntry entryHeader, Stream stream)
|
||||
{
|
||||
if (FlagUtility.HasFlag(entryHeader.Flags, HeaderFlags.Encrypted))
|
||||
{
|
||||
if (
|
||||
entryHeader.CompressedSize == 0
|
||||
&& FlagUtility.HasFlag(entryHeader.Flags, HeaderFlags.UsePostDataDescriptor)
|
||||
)
|
||||
{
|
||||
// The sizes are only in the data descriptor, which cannot be located by scanning
|
||||
// encrypted data; a seekable stream can take them from the central directory.
|
||||
await TryResolveDescriptorSizesAsync(entryHeader, stream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (
|
||||
!entryHeader.IsDirectory
|
||||
&& entryHeader.CompressedSize == 0
|
||||
|
||||
@@ -36,6 +36,16 @@ internal partial class ZipHeaderFactory
|
||||
_archiveEncoding = archiveEncoding;
|
||||
}
|
||||
|
||||
protected IArchiveEncoding ArchiveEncoding => _archiveEncoding;
|
||||
|
||||
/// <summary>
|
||||
/// Populates the sizes of an encrypted entry that was written with a post data descriptor and
|
||||
/// therefore has no sizes in its local header.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> when <see cref="ZipFileEntry.CompressedSize"/> was populated.</returns>
|
||||
protected virtual bool TryResolveDescriptorSizes(ZipFileEntry entryHeader, Stream stream) =>
|
||||
false;
|
||||
|
||||
protected ZipHeader? ReadHeader(uint headerBytes, BinaryReader reader, bool zip64 = false)
|
||||
{
|
||||
switch (headerBytes)
|
||||
@@ -133,10 +143,20 @@ internal partial class ZipHeaderFactory
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadHeader(ZipFileEntry entryHeader, Stream stream)
|
||||
protected void LoadHeader(ZipFileEntry entryHeader, Stream stream)
|
||||
{
|
||||
if (FlagUtility.HasFlag(entryHeader.Flags, HeaderFlags.Encrypted))
|
||||
{
|
||||
if (
|
||||
entryHeader.CompressedSize == 0
|
||||
&& FlagUtility.HasFlag(entryHeader.Flags, HeaderFlags.UsePostDataDescriptor)
|
||||
)
|
||||
{
|
||||
// The sizes are only in the data descriptor, which cannot be located by scanning
|
||||
// encrypted data; a seekable stream can take them from the central directory.
|
||||
TryResolveDescriptorSizes(entryHeader, stream);
|
||||
}
|
||||
|
||||
if (
|
||||
!entryHeader.IsDirectory
|
||||
&& entryHeader.CompressedSize == 0
|
||||
|
||||
347
tests/SharpCompress.Test/Zip/ZipEncryptedDataDescriptorTests.cs
Normal file
347
tests/SharpCompress.Test/Zip/ZipEncryptedDataDescriptorTests.cs
Normal file
@@ -0,0 +1,347 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.Zip;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Zip;
|
||||
|
||||
/// <summary>
|
||||
/// Archives written by streaming encryptors (these fixtures by Aspose.Zip) encrypt every entry, including
|
||||
/// directories, and set the post data descriptor flag with zero sizes in the local header. The sizes are
|
||||
/// only in the data descriptor and the central directory.
|
||||
/// </summary>
|
||||
public class ZipEncryptedDataDescriptorTests : TestBase
|
||||
{
|
||||
private const string Password = "test";
|
||||
|
||||
public static TheoryData<string> Archives =>
|
||||
new()
|
||||
{
|
||||
"Zip.deflate.zipcrypto.datadescriptor.zip",
|
||||
"Zip.deflate.aes128.datadescriptor.zip",
|
||||
"Zip.deflate.aes192.datadescriptor.zip",
|
||||
"Zip.deflate.aes256.datadescriptor.zip",
|
||||
"Zip.store.zipcrypto.datadescriptor.zip",
|
||||
"Zip.store.aes256.datadescriptor.zip",
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Archives))]
|
||||
public void Reader_Reads_All_Entries(string archive)
|
||||
{
|
||||
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archive));
|
||||
using var reader = ZipReader.OpenReader(stream, Options());
|
||||
|
||||
var contents = new Dictionary<string, byte[]>();
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
contents.Add(reader.Entry.Key!, reader.Entry.IsDirectory ? [] : Read(reader));
|
||||
}
|
||||
|
||||
AssertExpected(contents);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Archives))]
|
||||
public async ValueTask Reader_Reads_All_Entries_Async(string archive)
|
||||
{
|
||||
using Stream stream = new AsyncOnlyStream(
|
||||
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archive))
|
||||
);
|
||||
await using var reader = await ReaderFactory.OpenAsyncReader(stream, Options());
|
||||
|
||||
var contents = new Dictionary<string, byte[]>();
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
contents.Add(
|
||||
reader.Entry.Key!,
|
||||
reader.Entry.IsDirectory ? [] : await ReadAsync(reader)
|
||||
);
|
||||
}
|
||||
|
||||
AssertExpected(contents);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Archives))]
|
||||
public void ReaderFactory_Reads_Entries_When_Some_Are_Skipped(string archive)
|
||||
{
|
||||
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archive));
|
||||
using var reader = ReaderFactory.OpenReader(stream, Options().WithLookForHeader(true));
|
||||
|
||||
var keys = new List<string>();
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
keys.Add(reader.Entry.Key!);
|
||||
if (reader.Entry.Key == "data/sub/binary.bin")
|
||||
{
|
||||
// partially read, the rest is skipped by the reader
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
Assert.Equal(1, entryStream.Read(new byte[1], 0, 1));
|
||||
}
|
||||
else if (reader.Entry.Key == "data/empty.txt")
|
||||
{
|
||||
Assert.Empty(Read(reader));
|
||||
}
|
||||
// data/readme.txt is never opened
|
||||
}
|
||||
|
||||
Assert.Equal(ExpectedEntries.Keys.OrderBy(k => k), keys.OrderBy(k => k));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Archives))]
|
||||
public async ValueTask Reader_Reads_Entries_When_Some_Are_Skipped_Async(string archive)
|
||||
{
|
||||
using Stream stream = new AsyncOnlyStream(
|
||||
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archive))
|
||||
);
|
||||
await using var reader = await ReaderFactory.OpenAsyncReader(stream, Options());
|
||||
|
||||
string? readmeKey = null;
|
||||
var count = 0;
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
count++;
|
||||
if (reader.Entry.Key == "data/readme.txt")
|
||||
{
|
||||
readmeKey = reader.Entry.Key;
|
||||
Assert.Equal(ExpectedEntries[readmeKey], await ReadAsync(reader));
|
||||
}
|
||||
// every other entry is skipped without being opened
|
||||
}
|
||||
|
||||
Assert.Equal(ExpectedEntries.Count, count);
|
||||
Assert.NotNull(readmeKey);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Archives))]
|
||||
public void Reader_Reports_Sizes_From_Central_Directory(string archive)
|
||||
{
|
||||
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archive));
|
||||
using var reader = ZipReader.OpenReader(stream, Options());
|
||||
|
||||
var files = 0;
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
files++;
|
||||
Assert.Equal(ExpectedEntries[reader.Entry.Key!].Length, reader.Entry.Size);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(3, files);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Archives))]
|
||||
public void Archive_Reads_All_Entries(string archive)
|
||||
{
|
||||
using var zip = ZipArchive.OpenArchive(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, archive),
|
||||
Options()
|
||||
);
|
||||
|
||||
var contents = new Dictionary<string, byte[]>();
|
||||
foreach (var entry in zip.Entries)
|
||||
{
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
contents.Add(entry.Key!, []);
|
||||
continue;
|
||||
}
|
||||
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var output = new MemoryStream();
|
||||
entryStream.CopyTo(output);
|
||||
contents.Add(entry.Key!, output.ToArray());
|
||||
}
|
||||
|
||||
AssertExpected(contents);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Archives))]
|
||||
public async ValueTask Archive_Reads_All_Entries_Async(string archive)
|
||||
{
|
||||
using Stream stream = new AsyncOnlyStream(
|
||||
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archive))
|
||||
);
|
||||
await using var zip = await ZipArchive.OpenAsyncArchive(stream, Options());
|
||||
|
||||
var contents = new Dictionary<string, byte[]>();
|
||||
await foreach (var entry in zip.EntriesAsync)
|
||||
{
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
contents.Add(entry.Key!, []);
|
||||
continue;
|
||||
}
|
||||
|
||||
#if LEGACY_DOTNET
|
||||
using var entryStream = await entry.OpenEntryStreamAsync();
|
||||
#else
|
||||
await using var entryStream = await entry.OpenEntryStreamAsync();
|
||||
#endif
|
||||
using var output = new MemoryStream();
|
||||
await entryStream.CopyToAsync(output);
|
||||
contents.Add(entry.Key!, output.ToArray());
|
||||
}
|
||||
|
||||
AssertExpected(contents);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Archives))]
|
||||
public void Wrong_Password_Is_Rejected(string archive)
|
||||
{
|
||||
var options = Options() with { Password = "wrong" };
|
||||
var expected = archive.Contains("zipcrypto")
|
||||
? typeof(CryptographicException)
|
||||
: typeof(InvalidFormatException);
|
||||
|
||||
using (var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archive)))
|
||||
{
|
||||
using var reader = ZipReader.OpenReader(stream, options);
|
||||
var exception = Record.Exception(() =>
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Read(reader);
|
||||
}
|
||||
}
|
||||
});
|
||||
Assert.IsType(expected, exception);
|
||||
}
|
||||
|
||||
using var zip = ZipArchive.OpenArchive(Path.Combine(TEST_ARCHIVES_PATH, archive), options);
|
||||
var archiveException = Record.Exception(() =>
|
||||
{
|
||||
foreach (var entry in zip.Entries.Where(e => !e.IsDirectory))
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
entryStream.CopyTo(Stream.Null);
|
||||
}
|
||||
});
|
||||
Assert.IsType(expected, archiveException);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Archives))]
|
||||
public void NonSeekable_Stream_Is_Not_Supported(string archive)
|
||||
{
|
||||
// Without the central directory the encrypted entries cannot be delimited; this must fail
|
||||
// instead of silently ending the enumeration after the first entry.
|
||||
using Stream stream = new ForwardOnlyStream(
|
||||
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archive))
|
||||
);
|
||||
using var reader = ZipReader.OpenReader(stream, Options());
|
||||
|
||||
Assert.Throws<NotSupportedException>(() =>
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Read(reader);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Archives))]
|
||||
public async ValueTask NonSeekable_Stream_Is_Not_Supported_Async(string archive)
|
||||
{
|
||||
using Stream stream = new ForwardOnlyStream(
|
||||
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archive))
|
||||
);
|
||||
// ZipReader directly: ReaderFactory format detection would report the failure as an unknown format.
|
||||
await using var reader = (IAsyncReader)
|
||||
ZipReader.OpenReader(new AsyncOnlyStream(stream, disposeStream: false), Options());
|
||||
|
||||
await Assert.ThrowsAsync<NotSupportedException>(async () =>
|
||||
{
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
await ReadAsync(reader);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static ReaderOptions Options() => new() { Password = Password };
|
||||
|
||||
private static Dictionary<string, byte[]> ExpectedEntries =>
|
||||
new()
|
||||
{
|
||||
["data/"] = [],
|
||||
["data/readme.txt"] = Encoding.UTF8.GetBytes(
|
||||
string.Concat(
|
||||
Enumerable
|
||||
.Range(0, 100)
|
||||
.Select(i => $"line {i}: the quick brown fox jumps over the lazy dog\n")
|
||||
)
|
||||
),
|
||||
["data/empty.txt"] = [],
|
||||
["data/sub/"] = [],
|
||||
["data/sub/binary.bin"] = Binary(20000),
|
||||
};
|
||||
|
||||
internal static byte[] Binary(int length)
|
||||
{
|
||||
var bytes = new byte[length];
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
bytes[i] = (byte)(((uint)i * 2654435761u) >> 24);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static void AssertExpected(Dictionary<string, byte[]> contents)
|
||||
{
|
||||
var expected = ExpectedEntries;
|
||||
Assert.Equal(expected.Keys.OrderBy(k => k), contents.Keys.OrderBy(k => k));
|
||||
foreach (var entry in expected)
|
||||
{
|
||||
Assert.True(
|
||||
entry.Value.SequenceEqual(contents[entry.Key]),
|
||||
$"content mismatch for {entry.Key}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] Read(IReader reader)
|
||||
{
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
using var output = new MemoryStream();
|
||||
entryStream.CopyTo(output);
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private static async ValueTask<byte[]> ReadAsync(IAsyncReader reader)
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
#else
|
||||
await using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
#endif
|
||||
using var output = new MemoryStream();
|
||||
await entryStream.CopyToAsync(output);
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
tests/TestArchives/Archives/Zip.store.aes256.datadescriptor.zip
Normal file
BIN
tests/TestArchives/Archives/Zip.store.aes256.datadescriptor.zip
Normal file
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user