Merge remote-tracking branch 'origin/release' into fix/1441-sevenzip-libarchive-attributes

This commit is contained in:
Adam Hathcock
2026-10-13 09:13:55 +01:00
54 changed files with 2228 additions and 348 deletions

View File

@@ -81,6 +81,8 @@ using (var archive = ArchiveFactory.OpenArchive(parts))
Stored (compression method 0) comments are supported up to 16 MiB of unpacked data; larger or unknown-size comments throw `InvalidFormatException`. Comments using other compression methods are skipped without affecting entry extraction.
Truncated encrypted comment data throws `InvalidFormatException` on synchronous reads or `IncompleteArchiveException` on asynchronous reads. RAR5 KDF counts above 24 are rejected with `CryptographicException` before deriving encryption keys.
```csharp
using var archive = RarArchive.OpenArchive(
"encrypted.rar",
@@ -484,6 +486,10 @@ using (var archive = ZipArchive.OpenArchive("file.zip"))
`CheckCrc` validates archive-level payload checksums when the format stores reliable metadata, such as ZIP CRC32 values. Formats without payload checksums skip this validation. Decompressor integrity checks that are required to decode a stream may still fail even when `CheckCrc` is disabled.
WinZip AES authentication is always checked, including when `CheckCrc` is disabled. Complete or dispose entry streams to verify authentication; plaintext returned before completion must not be treated as authenticated. Invalid authentication tags throw `InvalidFormatException`, and missing ciphertext or tag bytes throw `IncompleteArchiveException`.
7z AES key derivation accepts iteration exponents 0 through 24 and the special `0x3F` direct-key mode. Larger iteration exponents throw `NotSupportedException` before key derivation, including when salt and IV are absent.
When using `SymbolicLinkHandler`, directory extraction rejects link targets outside the extraction root and never follows symbolic links or reparse points while extracting later entries. The handler itself remains trusted application code.
### Options matrix
@@ -831,3 +837,17 @@ using (var reader = ReaderFactory.OpenReader(stream))
- [USAGE.md](USAGE.md) - Complete code examples
- [FORMATS.md](FORMATS.md) - Supported formats
- [PERFORMANCE.md](PERFORMANCE.md) - API selection guide
## 7z Encrypted Decoding Errors
AES-encrypted 7z codec data errors and encrypted-header CRC failures throw `SharpCompress.Common.CryptographicException`,
with the original decoder error in `InnerException`. The exception can indicate either an
incorrect password or corrupt encrypted data; 7z AES has no independent password verifier.
`CryptographicException(string message, Exception inner)` preserves an underlying failure.
Unencrypted codec errors retain their existing behavior. This does not guarantee detection
of every incorrect password, particularly for data without compression or integrity checking.
## 7z Encryption Detection
For 7z archives, `IsEncrypted` is true when any entry has an AES-encrypted data stream.
Streamless empty files and directories are not encrypted. Archives without files return false.

View File

@@ -36,9 +36,17 @@
### Zip Format Notes
- Multi-volume/split ZIP archives require ZipArchive (seekable streams) as ZipReader cannot seek across volume files.
- ZIP/ZIPX companion volumes are discovered with a case-insensitive filename fallback, including on case-sensitive filesystems. An exact generated filename takes precedence; ambiguous fallback matches throw `InvalidFormatException`.
- WinZip AES authentication (HMAC-SHA1-80 over ciphertext) is verified independently of `CheckCrc`. Incorrect tags throw `InvalidFormatException`; truncated ciphertext or tags throw `IncompleteArchiveException`. Reading an entry to completion or disposing its stream verifies authentication.
- ZipReader processes entries from LocalEntry headers (which include directory entries ending with `/`) and intentionally skips DirectoryEntry headers from the central directory, as they are redundant in streaming mode - all entry data comes from LocalEntry headers which ZipReader has already processed.
- ZIP supports ZStandard for reading and writing. Tar.Zstandard support is read/decompress only.
### Rar Format Notes
- RAR5 encrypted headers and data accept KDF counts from 0 through 24 (the binary logarithm of the PBKDF2 iteration count), matching UnRAR's acceptance limit. Larger counts are rejected before key derivation.
- RAR companion filenames use the same exact-match-first, case-insensitive fallback as ZIP, with ambiguous fallback matches rejected.
- Encrypted ciphertext must contain complete AES blocks. Truncation throws `InvalidFormatException` on synchronous reads or `IncompleteArchiveException` on asynchronous reads.
### 7Zip Format Notes
- **Async Extraction Performance**: When using async extraction methods (e.g., `ExtractAllEntries()` with `MoveToNextEntryAsync()`), each file creates its own decompression stream to avoid state corruption in the LZMA decoder. This is less efficient than synchronous extraction, which can reuse a single decompression stream for multiple files in the same folder.

View File

@@ -28,11 +28,32 @@ internal abstract class ArchiveVolumeFactory
);
}
if (item != null && item.Exists)
return item is null ? null : ResolveFilePart(item);
}
internal static FileInfo? ResolveFilePart(FileInfo item)
{
if (item.Exists)
{
return item;
}
return null;
if (!item.Directory!.Exists)
{
return null;
}
FileInfo? match = null;
foreach (var file in item.Directory.EnumerateFiles())
{
if (!string.Equals(file.Name, item.Name, StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (match is not null)
{
throw new InvalidFormatException($"Ambiguous archive volume name: {item.Name}");
}
match = file;
}
return match;
}
}

View File

@@ -61,11 +61,6 @@ internal static class RarArchiveVolumeFactory
}
}
if (item != null && item.Exists)
{
return item;
}
return null; //no more items
return item is null ? null : ArchiveVolumeFactory.ResolveFilePart(item);
}
}

View File

@@ -98,7 +98,7 @@ public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, Sev
.GroupBy(x => x.FilePart.Folder)
.Any(folder => folder.Skip(1).Any());
public override bool IsEncrypted => Entries.First(x => !x.IsDirectory).IsEncrypted;
public override bool IsEncrypted => Entries.Any(x => x.IsEncrypted);
public override long TotalSize =>
_database?._packSizes.Aggregate(0L, (total, packSize) => total + packSize) ?? 0;

View File

@@ -32,11 +32,6 @@ internal static class ZipArchiveVolumeFactory
return ArchiveVolumeFactory.GetFilePart(index, part1);
}
if (item != null && item.Exists)
{
return item;
}
return null; //no more items
return item is null ? null : ArchiveVolumeFactory.ResolveFilePart(item);
}
}

View File

@@ -100,7 +100,7 @@ public abstract partial class AceHeader
// First two bytes are typically 0x60 0xEA (signature bytes)
// At offset 7, there should be "**ACE**" (7 bytes)
var bytes = new byte[14];
if (stream.Read(bytes, 0, 14) != 14)
if (!stream.ReadFully(bytes))
{
return false;
}

View File

@@ -26,7 +26,7 @@ public class ArcEntryHeader
public ArcEntryHeader? ReadHeader(Stream stream)
{
byte[] headerBytes = new byte[29];
if (stream.Read(headerBytes, 0, headerBytes.Length) != headerBytes.Length)
if (!stream.ReadFully(headerBytes))
{
return null;
}
@@ -40,11 +40,7 @@ public class ArcEntryHeader
)
{
byte[] headerBytes = new byte[29];
if (
await stream
.ReadAsync(headerBytes, 0, headerBytes.Length, cancellationToken)
.ConfigureAwait(false) != headerBytes.Length
)
if (!await stream.ReadFullyAsync(headerBytes, cancellationToken).ConfigureAwait(false))
{
return null;
}

View File

@@ -21,7 +21,7 @@ public abstract partial class ArjHeader
{
// check for magic bytes
var magic = new byte[2];
if (await stream.ReadAsync(magic, 0, 2, cancellationToken).ConfigureAwait(false) != 2)
if (!await stream.ReadFullyAsync(magic, cancellationToken).ConfigureAwait(false))
{
return Array.Empty<byte>();
}
@@ -33,7 +33,7 @@ public abstract partial class ArjHeader
// read header_size
byte[] headerBytes = new byte[2];
await stream.ReadAsync(headerBytes, 0, 2, cancellationToken).ConfigureAwait(false);
await stream.ReadExactAsync(headerBytes, 0, 2, cancellationToken).ConfigureAwait(false);
var headerSize = (ushort)(headerBytes[0] | headerBytes[1] << 8);
if (headerSize < 1)
{
@@ -41,16 +41,13 @@ public abstract partial class ArjHeader
}
var body = new byte[headerSize];
var read = await stream
.ReadAsync(body, 0, headerSize, cancellationToken)
.ConfigureAwait(false);
if (read < headerSize)
if (!await stream.ReadFullyAsync(body, cancellationToken).ConfigureAwait(false))
{
return Array.Empty<byte>();
}
byte[] crc = new byte[4];
await stream.ReadFullyAsync(crc, 0, 4, cancellationToken).ConfigureAwait(false);
await stream.ReadExactAsync(crc, 0, 4, cancellationToken).ConfigureAwait(false);
var checksum = Crc32Stream.Compute(body);
// Compute the hash value
if (checksum != BitConverter.ToUInt32(crc, 0))
@@ -70,10 +67,7 @@ public abstract partial class ArjHeader
while (true)
{
int bytesRead = await reader
.ReadAsync(buffer, 0, 2, cancellationToken)
.ConfigureAwait(false);
if (bytesRead < 2)
if (!await reader.ReadFullyAsync(buffer, cancellationToken).ConfigureAwait(false))
{
throw new IncompleteArchiveException(
"Unexpected end of stream while reading extended header size."
@@ -87,10 +81,7 @@ public abstract partial class ArjHeader
}
byte[] header = new byte[extHeaderSize];
bytesRead = await reader
.ReadAsync(header, 0, extHeaderSize, cancellationToken)
.ConfigureAwait(false);
if (bytesRead < extHeaderSize)
if (!await reader.ReadFullyAsync(header, cancellationToken).ConfigureAwait(false))
{
throw new IncompleteArchiveException(
"Unexpected end of stream while reading extended header data."
@@ -98,10 +89,7 @@ public abstract partial class ArjHeader
}
byte[] crcextended = new byte[4];
bytesRead = await reader
.ReadAsync(crcextended, 0, 4, cancellationToken)
.ConfigureAwait(false);
if (bytesRead < 4)
if (!await reader.ReadFullyAsync(crcextended, cancellationToken).ConfigureAwait(false))
{
throw new IncompleteArchiveException(
"Unexpected end of stream while reading extended header CRC."
@@ -130,7 +118,7 @@ public abstract partial class ArjHeader
)
{
var bytes = new byte[2];
if (await stream.ReadAsync(bytes, 0, 2, cancellationToken).ConfigureAwait(false) != 2)
if (!await stream.ReadFullyAsync(bytes, cancellationToken).ConfigureAwait(false))
{
return false;
}

View File

@@ -37,7 +37,7 @@ public abstract partial class ArjHeader
{
// check for magic bytes
var magic = new byte[2];
if (stream.Read(magic) != 2)
if (!stream.ReadFully(magic))
{
return Array.Empty<byte>();
}
@@ -49,7 +49,7 @@ public abstract partial class ArjHeader
// read header_size
byte[] headerBytes = new byte[2];
stream.Read(headerBytes, 0, 2);
stream.ReadExact(headerBytes, 0, 2);
var headerSize = (ushort)(headerBytes[0] | headerBytes[1] << 8);
if (headerSize < 1)
{
@@ -57,14 +57,13 @@ public abstract partial class ArjHeader
}
var body = new byte[headerSize];
var read = stream.Read(body, 0, headerSize);
if (read < headerSize)
if (!stream.ReadFully(body))
{
return Array.Empty<byte>();
}
byte[] crc = new byte[4];
read = stream.Read(crc, 0, 4);
stream.ReadExact(crc, 0, 4);
var checksum = Crc32Stream.Compute(body);
// Compute the hash value
if (checksum != BitConverter.ToUInt32(crc, 0))
@@ -83,8 +82,7 @@ public abstract partial class ArjHeader
while (true)
{
int bytesRead = reader.Read(buffer, 0, 2);
if (bytesRead < 2)
if (!reader.ReadFully(buffer))
{
throw new IncompleteArchiveException(
"Unexpected end of stream while reading extended header size."
@@ -98,8 +96,7 @@ public abstract partial class ArjHeader
}
byte[] header = new byte[extHeaderSize];
bytesRead = reader.Read(header, 0, extHeaderSize);
if (bytesRead < extHeaderSize)
if (!reader.ReadFully(header))
{
throw new IncompleteArchiveException(
"Unexpected end of stream while reading extended header data."
@@ -107,8 +104,7 @@ public abstract partial class ArjHeader
}
byte[] crc = new byte[4];
bytesRead = reader.Read(crc, 0, 4);
if (bytesRead < 4)
if (!reader.ReadFully(crc))
{
throw new IncompleteArchiveException(
"Unexpected end of stream while reading extended header CRC."
@@ -147,7 +143,7 @@ public abstract partial class ArjHeader
public static bool IsArchive(Stream stream)
{
var bytes = new byte[2];
if (stream.Read(bytes, 0, 2) != 2)
if (!stream.ReadFully(bytes))
{
return false;
}

View File

@@ -43,7 +43,10 @@ internal sealed class ChecksumValidationStream : Stream
public override int Read(byte[] buffer, int offset, int count)
{
var read = _stream.Read(buffer, offset, count);
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
if (count != 0)
{
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
}
return read;
}
@@ -51,7 +54,10 @@ internal sealed class ChecksumValidationStream : Stream
public override int Read(Span<byte> buffer)
{
var read = _stream.Read(buffer);
UpdateAndValidateAtEof(buffer[..read], read);
if (!buffer.IsEmpty)
{
UpdateAndValidateAtEof(buffer[..read], read);
}
return read;
}
#endif
@@ -68,7 +74,10 @@ internal sealed class ChecksumValidationStream : Stream
var read = await _stream
.ReadAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
if (count != 0)
{
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
}
return read;
}
@@ -79,7 +88,10 @@ internal sealed class ChecksumValidationStream : Stream
)
{
var read = await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
UpdateAndValidateAtEof(buffer.Span[..read], read);
if (!buffer.IsEmpty)
{
UpdateAndValidateAtEof(buffer.Span[..read], read);
}
return read;
}
#endif

View File

@@ -18,13 +18,15 @@ internal sealed class GZipChecksumValidationStream : Stream
private uint _seed = Crc32Stream.DEFAULT_SEED;
private uint _size;
private bool _validated;
private readonly Func<Stream?>? _bufferedInput;
internal GZipChecksumValidationStream(
Stream source,
Stream rawStream,
string? entryName,
uint? expectedCrc,
uint? expectedSize
uint? expectedSize,
Func<Stream?>? bufferedInput = null
)
{
_source = source;
@@ -32,6 +34,7 @@ internal sealed class GZipChecksumValidationStream : Stream
_entryName = string.IsNullOrEmpty(entryName) ? "Entry" : entryName!;
_expectedCrc = expectedCrc;
_expectedSize = expectedSize;
_bufferedInput = bufferedInput;
_crc32Table = Crc32Stream.InitializeTable(Crc32Stream.DEFAULT_POLYNOMIAL);
}
@@ -54,7 +57,10 @@ internal sealed class GZipChecksumValidationStream : Stream
public override int Read(byte[] buffer, int offset, int count)
{
var read = _source.Read(buffer, offset, count);
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
if (count != 0)
{
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
}
return read;
}
@@ -62,7 +68,10 @@ internal sealed class GZipChecksumValidationStream : Stream
public override int Read(Span<byte> buffer)
{
var read = _source.Read(buffer);
UpdateAndValidateAtEof(buffer[..read], read);
if (!buffer.IsEmpty)
{
UpdateAndValidateAtEof(buffer[..read], read);
}
return read;
}
#endif
@@ -93,7 +102,17 @@ internal sealed class GZipChecksumValidationStream : Stream
var read = await _source
.ReadAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
if (count != 0)
{
if (read == 0)
{
await ValidateAsync(cancellationToken).ConfigureAwait(false);
}
else
{
UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
}
}
return read;
}
@@ -104,7 +123,17 @@ internal sealed class GZipChecksumValidationStream : Stream
)
{
var read = await _source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
UpdateAndValidateAtEof(buffer.Span[..read], read);
if (!buffer.IsEmpty)
{
if (read == 0)
{
await ValidateAsync(cancellationToken).ConfigureAwait(false);
}
else
{
UpdateAndValidateAtEof(buffer.Span[..read], read);
}
}
return read;
}
#endif
@@ -135,31 +164,77 @@ internal sealed class GZipChecksumValidationStream : Stream
return;
}
_validated = true;
var expectedCrc = _expectedCrc;
var expectedSize = _expectedSize;
if (!expectedCrc.HasValue || !expectedSize.HasValue)
{
Span<byte> trailer = stackalloc byte[8];
_rawStream.ReadFully(trailer);
var bufferedCount = ReadBufferedTrailer(trailer);
if (!_rawStream.ReadFully(trailer.Slice(bufferedCount)))
{
throw new IncompleteArchiveException("Unexpected end of GZIP trailer.");
}
expectedCrc = BinaryPrimitives.ReadUInt32LittleEndian(trailer);
expectedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer[4..]);
}
ValidateValues(expectedCrc.Value, expectedSize.Value);
}
private async ValueTask ValidateAsync(CancellationToken cancellationToken)
{
if (_validated)
{
return;
}
var expectedCrc = _expectedCrc;
var expectedSize = _expectedSize;
if (!expectedCrc.HasValue || !expectedSize.HasValue)
{
var trailer = new byte[8];
var bufferedCount = ReadBufferedTrailer(trailer);
if (
!await _rawStream
.ReadFullyAsync(
trailer,
bufferedCount,
trailer.Length - bufferedCount,
cancellationToken
)
.ConfigureAwait(false)
)
{
throw new IncompleteArchiveException("Unexpected end of GZIP trailer.");
}
expectedCrc = BinaryPrimitives.ReadUInt32LittleEndian(trailer);
expectedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer.AsSpan(4));
}
ValidateValues(expectedCrc.Value, expectedSize.Value);
}
private int ReadBufferedTrailer(Span<byte> trailer)
{
// The inflater may have read ahead beyond the final compressed bit.
using var buffered = _bufferedInput?.Invoke();
return buffered?.Read(trailer) ?? 0;
}
private void ValidateValues(uint expectedCrc, uint expectedSize)
{
var actualCrc = ~_seed;
if (actualCrc != expectedCrc.Value)
if (actualCrc != expectedCrc)
{
throw new InvalidFormatException(
$"CRC mismatch for entry '{_entryName}'. Expected 0x{expectedCrc.Value:X8}, actual 0x{actualCrc:X8}."
$"CRC mismatch for entry '{_entryName}'. Expected 0x{expectedCrc:X8}, actual 0x{actualCrc:X8}."
);
}
if (_size != expectedSize.Value)
if (_size != expectedSize)
{
throw new InvalidFormatException(
$"Size mismatch for entry '{_entryName}'. Expected {expectedSize.Value}, actual {_size}."
$"Size mismatch for entry '{_entryName}'. Expected {expectedSize}, actual {_size}."
);
}
_validated = true;
}
}

View File

@@ -44,7 +44,10 @@ internal sealed partial class GZipFilePart
{
// Read and potentially verify the GZIP trailer: CRC32 and size mod 2^32
var trailer = new byte[8];
_ = await _stream.ReadFullyAsync(trailer, 0, 8, cancellationToken).ConfigureAwait(false);
if (!await _stream.ReadFullyAsync(trailer, cancellationToken).ConfigureAwait(false))
{
throw new ZlibException("Unexpected end-of-file reading GZIP trailer.");
}
Crc = BinaryPrimitives.ReadUInt32LittleEndian(trailer);
UncompressedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer.AsSpan().Slice(4));
@@ -64,7 +67,11 @@ internal sealed partial class GZipFilePart
return;
}
if (n != 10)
if (
!await _stream
.ReadFullyAsync(header, n, header.Length - n, cancellationToken)
.ConfigureAwait(false)
)
{
throw new ZlibException("Not a valid GZIP stream.");
}
@@ -80,7 +87,10 @@ internal sealed partial class GZipFilePart
{
// read and discard extra field
var lengthField = new byte[2];
_ = await _stream.ReadAsync(lengthField, 0, 2, cancellationToken).ConfigureAwait(false);
if (!await _stream.ReadFullyAsync(lengthField, cancellationToken).ConfigureAwait(false))
{
throw new ZlibException("Unexpected end-of-file reading GZIP header.");
}
var extraLength = lengthField[0] + (lengthField[1] * 256);
var extra = new byte[extraLength];
@@ -101,8 +111,11 @@ internal sealed partial class GZipFilePart
}
if ((header[3] & 0x02) == 0x02)
{
var buf = new byte[1];
_ = await _stream.ReadAsync(buf, 0, 1, cancellationToken).ConfigureAwait(false); // CRC16, ignore
var headerCrc = new byte[2];
if (!await _stream.ReadFullyAsync(headerCrc, cancellationToken).ConfigureAwait(false))
{
throw new ZlibException("Unexpected end-of-file reading GZIP header CRC.");
}
}
}
@@ -140,8 +153,9 @@ internal sealed partial class GZipFilePart
)
{
// GZip uses Deflate compression
return await _compressionProviders
_decompressor = await _compressionProviders
.CreateDecompressStreamAsync(CompressionType.Deflate, _stream, cancellationToken)
.ConfigureAwait(false);
return _decompressor;
}
}

View File

@@ -15,6 +15,7 @@ internal sealed partial class GZipFilePart : FilePart
private string? _name;
private readonly Stream _stream;
private readonly CompressionProviderRegistry _compressionProviders;
private Stream? _decompressor;
internal static GZipFilePart Create(
Stream stream,
@@ -64,11 +65,22 @@ internal sealed partial class GZipFilePart : FilePart
internal override Stream GetCompressedStream()
{
//GZip uses Deflate compression, at this point we need a deflate stream
return _compressionProviders.CreateDecompressStream(CompressionType.Deflate, _stream);
_decompressor = _compressionProviders.CreateDecompressStream(
CompressionType.Deflate,
_stream
);
return _decompressor;
}
internal Stream WrapWithChecksumValidation(Stream source, string? entryName) =>
new GZipChecksumValidationStream(source, _stream, entryName, Crc, UncompressedSize);
new GZipChecksumValidationStream(
source,
_stream,
entryName,
Crc,
UncompressedSize,
() => (_decompressor as DeflateStream)?.InputBuffer
);
internal override Stream GetRawStream() => _stream;
@@ -76,7 +88,10 @@ internal sealed partial class GZipFilePart : FilePart
{
// Read and potentially verify the GZIP trailer: CRC32 and size mod 2^32
Span<byte> trailer = stackalloc byte[8];
_stream.ReadFully(trailer);
if (!_stream.ReadFully(trailer))
{
throw new ZlibException("Unexpected end-of-file reading GZIP trailer.");
}
Crc = BinaryPrimitives.ReadUInt32LittleEndian(trailer);
UncompressedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer.Slice(4));
@@ -94,7 +109,7 @@ internal sealed partial class GZipFilePart : FilePart
return;
}
if (n != 10)
if (!_stream.ReadFully(header.Slice(n)))
{
throw new ZlibException("Not a valid GZIP stream.");
}
@@ -109,7 +124,10 @@ internal sealed partial class GZipFilePart : FilePart
if ((header[3] & 0x04) == 0x04)
{
// read and discard extra field
n = _stream.Read(header.Slice(0, 2)); // 2-byte length field
if (!_stream.ReadFully(header.Slice(0, 2)))
{
throw new ZlibException("Unexpected end-of-file reading GZIP header.");
}
var extraLength = header[0] + (header[1] * 256);
var extra = new byte[extraLength];
@@ -130,7 +148,11 @@ internal sealed partial class GZipFilePart : FilePart
}
if ((header[3] & 0x02) == 0x02)
{
_stream.ReadByte(); // CRC16, ignore
Span<byte> headerCrc = stackalloc byte[2];
if (!_stream.ReadFully(headerCrc))
{
throw new ZlibException("Unexpected end-of-file reading GZIP header CRC.");
}
}
}

View File

@@ -53,7 +53,7 @@ internal static class EncryptionFlagsV5
internal static class EncryptionConstV5
{
public const int VERSION = 0;
public const uint CRYPT5_KDF_LG2_COUNT_MAX = 0x24;
public const uint CRYPT5_KDF_LG2_COUNT_MAX = 24;
public const int SIZE_SALT30 = 0x08;
public const int SIZE_SALT50 = 0x10;
public const int SIZE_INITV = 0x10;

View File

@@ -24,7 +24,7 @@ internal class Rar5CryptoInfo
encryptionFlags,
EncryptionFlagsV5.CHFL_CRYPT_PSWCHECK
);
cryptoInfo.LG2Count = reader.ReadRarVIntByte(1);
cryptoInfo.LG2Count = reader.ReadByte();
if (cryptoInfo.LG2Count > EncryptionConstV5.CRYPT5_KDF_LG2_COUNT_MAX)
{
@@ -78,10 +78,9 @@ internal class Rar5CryptoInfo
encryptionFlags,
EncryptionFlagsV5.CHFL_CRYPT_PSWCHECK
);
cryptoInfo.LG2Count = (int)
await reader
.ReadRarVIntUInt32Async(cancellationToken: CancellationToken.None)
.ConfigureAwait(false);
cryptoInfo.LG2Count = await reader
.ReadByteAsync(CancellationToken.None)
.ConfigureAwait(false);
if (cryptoInfo.LG2Count > EncryptionConstV5.CRYPT5_KDF_LG2_COUNT_MAX)
{
throw new CryptographicException($"Unsupported LG2 count of {cryptoInfo.LG2Count}.");

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using SharpCompress.Common.Rar.Headers;
namespace SharpCompress.Common.Rar;
@@ -12,6 +13,12 @@ internal sealed class Rar5KeyCache
internal List<byte[]> GetKeys(string password, byte[] salt, int lg2Count)
{
// Metadata can be mutated after parsing; validate before C# masks the shift count.
if (lg2Count < 0 || lg2Count > EncryptionConstV5.CRYPT5_KDF_LG2_COUNT_MAX)
{
throw new CryptographicException($"Unsupported LG2 count of {lg2Count}.");
}
lock (entries)
{
foreach (var (cachedPassword, cachedSalt, cachedCount, cachedKeys) in entries)

View File

@@ -40,8 +40,11 @@ internal sealed class RarCryptoWrapper : Stream
Span<byte> cipherText = stackalloc byte[16];
for (var i = 0; i < alignedSize / 16; i++)
{
//long ax = System.currentTimeMillis();
_actualStream.Read(cipherText);
if (!_actualStream.ReadFully(cipherText))
{
// Preserve the synchronous API's malformed-archive exception type.
throw new InvalidFormatException("Unexpected end of encrypted stream");
}
var readBytes = _rijndael.ProcessBlock(cipherText);
foreach (var readByte in readBytes)
@@ -49,11 +52,11 @@ internal sealed class RarCryptoWrapper : Stream
_data.Enqueue(readByte);
}
}
}
for (var i = 0; i < count; i++)
{
buffer[offset + i] = _data.Dequeue();
}
for (var i = 0; i < count; i++)
{
buffer[offset + i] = _data.Dequeue();
}
return count;
}
@@ -97,7 +100,7 @@ internal sealed class RarCryptoWrapper : Stream
}
}
}
catch (EndOfStreamException e)
catch (IncompleteArchiveException e)
{
throw new InvalidFormatException("Unexpected end of encrypted stream", e);
}

View File

@@ -231,9 +231,12 @@ internal sealed partial class ArchiveReader
!= folder._unpackCrc
)
{
throw new InvalidFormatException(
var exception = new InvalidFormatException(
"Decoded stream does not match expected CRC."
);
throw folder.IsEncrypted()
? EncryptedDecoderStream.DecodingException(exception)
: exception;
}
}
}

View File

@@ -674,9 +674,12 @@ internal partial class ArchiveReader
!= folder._unpackCrc
)
{
throw new InvalidFormatException(
var exception = new InvalidFormatException(
"Decoded stream does not match expected CRC."
);
throw folder.IsEncrypted()
? EncryptedDecoderStream.DecodingException(exception)
: exception;
}
}
}

View File

@@ -119,7 +119,5 @@ internal class SevenZipFilePart : FilePart
};
}
internal bool IsEncrypted =>
!Header.IsDir
&& Folder?._coders.FindIndex(c => c._methodId._id == CMethodId.K_AES_ID) != -1;
internal bool IsEncrypted => !Header.IsDir && Folder?.IsEncrypted() == true;
}

View File

@@ -28,7 +28,14 @@ public class ArchiveOperationException : SharpCompressException
public class IncompleteArchiveException(string message) : ArchiveException(message);
public class CryptographicException(string message) : SharpCompressException(message);
public class CryptographicException : SharpCompressException
{
public CryptographicException(string message)
: base(message) { }
public CryptographicException(string message, Exception inner)
: base(message, inner) { }
}
public class ReaderCancelledException(string message) : SharpCompressException(message);

View File

@@ -260,48 +260,48 @@ internal sealed partial class TarHeader
break;
}
// Check header checksum
if (!checkChecksum(buffer))
{
return false;
}
Name = ArchiveEncoding.Decode(buffer, 0, 100).TrimNulls();
EntryType = entryType;
Size = ReadSize(buffer);
LinkName = null;
// for symlinks, additionally read the linkname
if (entryType == EntryType.SymLink || entryType == EntryType.HardLink)
{
LinkName = ArchiveEncoding.Decode(buffer, 157, 100).TrimNulls();
}
Mode = ReadAsciiInt64Base8(buffer, 100, 7);
UserId = ReadAsciiInt64Base8oldGnu(buffer, 108, 7);
GroupId = ReadAsciiInt64Base8oldGnu(buffer, 116, 7);
var unixTimeStamp = ReadAsciiInt64Base8(buffer, 136, 11);
LastModifiedTime = EPOCH.AddSeconds(unixTimeStamp).ToLocalTime();
Magic = ArchiveEncoding.Decode(buffer, 257, 6).TrimNulls();
if (!string.IsNullOrEmpty(Magic) && "ustar".Equals(Magic, StringComparison.Ordinal))
{
var namePrefix = ArchiveEncoding.Decode(buffer, 345, 157).TrimNulls();
if (!string.IsNullOrEmpty(namePrefix))
{
Name = namePrefix + "/" + Name;
}
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
// Check header checksum
if (!checkChecksum(buffer))
{
return false;
}
Name = ArchiveEncoding.Decode(buffer, 0, 100).TrimNulls();
EntryType = entryType;
Size = ReadSize(buffer);
LinkName = null;
// for symlinks, additionally read the linkname
if (entryType == EntryType.SymLink || entryType == EntryType.HardLink)
{
LinkName = ArchiveEncoding.Decode(buffer, 157, 100).TrimNulls();
}
Mode = ReadAsciiInt64Base8(buffer, 100, 7);
UserId = ReadAsciiInt64Base8oldGnu(buffer, 108, 7);
GroupId = ReadAsciiInt64Base8oldGnu(buffer, 116, 7);
var unixTimeStamp = ReadAsciiInt64Base8(buffer, 136, 11);
LastModifiedTime = EPOCH.AddSeconds(unixTimeStamp).ToLocalTime();
Magic = ArchiveEncoding.Decode(buffer, 257, 6).TrimNulls();
if (!string.IsNullOrEmpty(Magic) && "ustar".Equals(Magic, StringComparison.Ordinal))
{
var namePrefix = ArchiveEncoding.Decode(buffer, 345, 157).TrimNulls();
if (!string.IsNullOrEmpty(namePrefix))
{
Name = namePrefix + "/" + Name;
}
}
pendingMetadata.ApplyTo(this);
if (entryType == EntryType.Directory)

View File

@@ -1,6 +1,8 @@
using System;
using System.Buffers;
using System.IO;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -17,21 +19,57 @@ internal partial class WinzipAesCryptoStream
return;
}
_isDisposed = true;
// Read out last 10 auth bytes asynchronously
byte[] authBytes = ArrayPool<byte>.Shared.Rent(10);
try
{
await _stream.ReadFullyAsync(authBytes, 0, 10).ConfigureAwait(false);
if (!_authRead)
{
await DrainAndVerifyAsync().ConfigureAwait(false);
}
}
finally
{
ArrayPool<byte>.Shared.Return(authBytes);
_authentication.Dispose();
_transform.Dispose();
_cipher.Dispose();
await _stream.DisposeAsync().ConfigureAwait(false);
}
await base.DisposeAsync().ConfigureAwait(false);
}
#endif
private async ValueTask VerifyAuthenticationAsync(CancellationToken cancellationToken)
{
_authenticationFailure?.Throw();
if (_authRead)
{
return;
}
_authRead = true;
var tag = new byte[10];
try
{
await _stream
.ReadExactAsync(tag, 0, tag.Length, cancellationToken)
.ConfigureAwait(false);
ValidateAuthentication(tag);
}
catch (Exception exception)
{
_authenticationFailure = ExceptionDispatchInfo.Capture(exception);
throw;
}
}
private async ValueTask DrainAndVerifyAsync()
{
var buffer = new byte[4096];
while (_totalBytesLeftToRead > 0)
{
await ReadAsync(buffer, 0, buffer.Length, CancellationToken.None).ConfigureAwait(false);
}
await VerifyAuthenticationAsync(CancellationToken.None).ConfigureAwait(false);
}
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
@@ -39,8 +77,15 @@ internal partial class WinzipAesCryptoStream
CancellationToken cancellationToken
)
{
cancellationToken.ThrowIfCancellationRequested();
_authenticationFailure?.Throw();
if (count == 0)
{
return 0;
}
if (_totalBytesLeftToRead == 0)
{
await VerifyAuthenticationAsync(cancellationToken).ConfigureAwait(false);
return 0;
}
var bytesToRead = count;
@@ -51,7 +96,17 @@ internal partial class WinzipAesCryptoStream
var read = await _stream
.ReadAsync(buffer, offset, bytesToRead, cancellationToken)
.ConfigureAwait(false);
if (read == 0)
{
_authRead = true;
throw new IncompleteArchiveException("Unexpected end of WinZip AES ciphertext.");
}
_authentication.TransformBlock(buffer, offset, read, null, 0);
_totalBytesLeftToRead -= read;
if (_totalBytesLeftToRead == 0)
{
await VerifyAuthenticationAsync(cancellationToken).ConfigureAwait(false);
}
ReadTransformBlocks(buffer, offset, read);
@@ -64,56 +119,22 @@ internal partial class WinzipAesCryptoStream
CancellationToken cancellationToken = default
)
{
if (_totalBytesLeftToRead == 0)
if (MemoryMarshal.TryGetArray((ReadOnlyMemory<byte>)buffer, out var segment))
{
return 0;
return await ReadAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken)
.ConfigureAwait(false);
}
var bytesToRead = buffer.Length;
if (buffer.Length > _totalBytesLeftToRead)
var rented = ArrayPool<byte>.Shared.Rent(buffer.Length);
try
{
bytesToRead = (int)_totalBytesLeftToRead;
var read = await ReadAsync(rented, 0, buffer.Length, cancellationToken)
.ConfigureAwait(false);
rented.AsSpan(0, read).CopyTo(buffer.Span);
return read;
}
var read = await _stream
.ReadAsync(buffer.Slice(0, bytesToRead), cancellationToken)
.ConfigureAwait(false);
_totalBytesLeftToRead -= read;
ReadTransformBlocks(buffer.Span, read);
return read;
}
private void ReadTransformBlocks(Span<byte> buffer, int count)
{
var posn = 0;
var remaining = count;
while (posn < buffer.Length && remaining > 0)
finally
{
var n = ReadTransformOneBlock(buffer, posn, remaining);
posn += n;
remaining -= n;
}
}
private int ReadTransformOneBlock(Span<byte> buffer, int offset, int remaining)
{
if (_counterOutOffset == BLOCK_SIZE_IN_BYTES)
{
FillCounterOut();
}
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, int counterOffset)
{
for (var i = 0; i < count; i++)
{
buffer[offset + i] = (byte)(_counterOut[counterOffset + i] ^ buffer[offset + i]);
ArrayPool<byte>.Shared.Return(rented, clearArray: true);
}
}
#endif

View File

@@ -1,9 +1,8 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.IO;
using System.Runtime.ExceptionServices;
using System.Security.Cryptography;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip;
@@ -14,6 +13,9 @@ internal partial class WinzipAesCryptoStream : Stream
private readonly byte[] _counter = new byte[BLOCK_SIZE_IN_BYTES];
private readonly Stream _stream;
private readonly ICryptoTransform _transform;
private readonly HMACSHA1 _authentication;
private bool _authRead;
private ExceptionDispatchInfo? _authenticationFailure;
private int _nonce = 1;
private byte[] _counterOut = new byte[BLOCK_SIZE_IN_BYTES];
private int _counterOutOffset = BLOCK_SIZE_IN_BYTES;
@@ -28,6 +30,14 @@ internal partial class WinzipAesCryptoStream : Stream
{
_stream = stream;
_totalBytesLeftToRead = length;
if (length < 0)
{
throw new InvalidFormatException("Invalid WinZip AES payload size.");
}
// PBKDF2's second key is the authentication key, not the CTR initialization vector.
#pragma warning disable CA5350 // WinZip AES mandates HMAC-SHA1-80 for interoperability.
_authentication = new HMACSHA1(winzipAesEncryptionData.IvBytes);
#pragma warning restore CA5350
_cipher = CreateCipher(winzipAesEncryptionData);
@@ -69,45 +79,85 @@ internal partial class WinzipAesCryptoStream : Stream
_isDisposed = true;
if (disposing)
{
// Read out last 10 auth bytes - catch exceptions for async-only streams
if (Utility.UseSyncOverAsyncDispose())
try
{
var ten = ArrayPool<byte>.Shared.Rent(10);
try
if (!_authRead && Utility.UseSyncOverAsyncDispose())
{
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
#pragma warning disable CA2012
_stream.ReadFullyAsync(ten, 0, 10).GetAwaiter().GetResult();
DrainAndVerifyAsync().GetAwaiter().GetResult();
#pragma warning restore CA2012
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
}
finally
else if (!_authRead)
{
ArrayPool<byte>.Shared.Return(ten);
var buffer = new byte[4096];
while (_totalBytesLeftToRead > 0)
{
Read(buffer, 0, buffer.Length);
}
VerifyAuthentication();
}
}
else
finally
{
Span<byte> ten = stackalloc byte[10];
_stream.ReadFully(ten);
_authentication.Dispose();
_transform.Dispose();
_cipher.Dispose();
_stream.Dispose();
}
_stream.Dispose();
}
base.Dispose(disposing);
}
private async ValueTask ReadAuthBytesAsync()
private void VerifyAuthentication()
{
byte[] authBytes = new byte[10];
await _stream.ReadFullyAsync(authBytes, 0, 10).ConfigureAwait(false);
_authenticationFailure?.Throw();
if (_authRead)
{
return;
}
_authRead = true;
var tag = new byte[10];
try
{
_stream.ReadExact(tag, 0, tag.Length);
ValidateAuthentication(tag);
}
catch (Exception exception)
{
_authenticationFailure = ExceptionDispatchInfo.Capture(exception);
throw;
}
}
private void ValidateAuthentication(byte[] tag)
{
_authentication.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
var expected = _authentication.Hash!;
var difference = 0;
for (var i = 0; i < tag.Length; i++)
{
difference |= tag[i] ^ expected[i];
}
if (difference != 0)
{
throw new InvalidFormatException("WinZip AES authentication failed.");
}
}
public override void Flush() { }
public override int Read(byte[] buffer, int offset, int count)
{
_authenticationFailure?.Throw();
if (count == 0)
{
return 0;
}
if (_totalBytesLeftToRead == 0)
{
VerifyAuthentication();
return 0;
}
var bytesToRead = count;
@@ -116,7 +166,17 @@ internal partial class WinzipAesCryptoStream : Stream
bytesToRead = (int)_totalBytesLeftToRead;
}
var read = _stream.Read(buffer, offset, bytesToRead);
if (read == 0)
{
_authRead = true;
throw new IncompleteArchiveException("Unexpected end of WinZip AES ciphertext.");
}
_authentication.TransformBlock(buffer, offset, read, null, 0);
_totalBytesLeftToRead -= read;
if (_totalBytesLeftToRead == 0)
{
VerifyAuthentication();
}
ReadTransformBlocks(buffer, offset, read);

View File

@@ -160,8 +160,8 @@ internal partial class ZipHeaderFactory
var salt = new byte[WinzipAesEncryptionData.KeyLengthInBytes(keySize) / 2];
var passwordVerifyValue = new byte[2];
stream.Read(salt, 0, salt.Length);
stream.Read(passwordVerifyValue, 0, 2);
stream.ReadExact(salt, 0, salt.Length);
stream.ReadExact(passwordVerifyValue, 0, 2);
entryHeader.WinzipAesEncryptionData = new WinzipAesEncryptionData(
keySize,
salt,

View File

@@ -327,7 +327,7 @@ internal class ZlibBaseStream : Stream, IStreamStack
if (!_wantCompress)
{
// workitem 8501: handle edge case (decompress empty stream)
if (_z.TotalBytesOut == 0L)
if (_z.TotalBytesOut == 0L && !_readCompleted)
{
return;
}
@@ -336,23 +336,14 @@ internal class ZlibBaseStream : Stream, IStreamStack
Span<byte> trailer = stackalloc byte[8];
// workitem 8679
if (_z.AvailableBytesIn != 8)
if (_z.AvailableBytesIn < 8)
{
// Make sure we have read to the end of the stream
_z.InputBuffer.AsSpan(_z.NextIn, _z.AvailableBytesIn).CopyTo(trailer);
var bytesNeeded = 8 - _z.AvailableBytesIn;
var bytesRead = _stream.Read(
trailer.Slice(_z.AvailableBytesIn, bytesNeeded)
);
if (bytesNeeded != bytesRead)
if (!_stream.ReadFully(trailer.Slice(_z.AvailableBytesIn, bytesNeeded)))
{
throw new ZlibException(
String.Format(
Constants.DefaultCultureInfo,
"Protocol error. AvailableBytesIn={0}, expected 8",
_z.AvailableBytesIn + bytesRead
)
);
throw new ZlibException("Unexpected end-of-file reading GZIP trailer.");
}
}
else
@@ -360,34 +351,7 @@ internal class ZlibBaseStream : Stream, IStreamStack
_z.InputBuffer.AsSpan(_z.NextIn, trailer.Length).CopyTo(trailer);
}
var crc32_expected = BinaryPrimitives.ReadInt32LittleEndian(trailer);
var crc32_actual = crc.Crc32Result;
var isize_expected = BinaryPrimitives.ReadInt32LittleEndian(trailer.Slice(4));
var isize_actual = (Int32)(_z.TotalBytesOut & 0x00000000FFFFFFFF);
if (crc32_actual != crc32_expected)
{
throw new ZlibException(
String.Format(
Constants.DefaultCultureInfo,
"Bad CRC32 in GZIP stream. (actual({0:X8})!=expected({1:X8}))",
crc32_actual,
crc32_expected
)
);
}
if (isize_actual != isize_expected)
{
throw new ZlibException(
String.Format(
Constants.DefaultCultureInfo,
"Bad size in GZIP stream. (actual({0})!=expected({1}))",
isize_actual,
isize_expected
)
);
}
ValidateGzipTrailer(trailer);
}
else
{
@@ -477,7 +441,7 @@ internal class ZlibBaseStream : Stream, IStreamStack
if (!_wantCompress)
{
// workitem 8501: handle edge case (decompress empty stream)
if (_z.TotalBytesOut == 0L)
if (_z.TotalBytesOut == 0L && !_readCompleted)
{
return;
}
@@ -486,15 +450,30 @@ internal class ZlibBaseStream : Stream, IStreamStack
byte[] trailer = new byte[8];
// workitem 8679
if (_z.AvailableBytesIn != 8)
if (_z.AvailableBytesIn < 8)
{
// Make sure we have read to the end of the stream
_z.InputBuffer.AsSpan(_z.NextIn, _z.AvailableBytesIn).CopyTo(trailer);
var bytesNeeded = 8 - _z.AvailableBytesIn;
var bytesRead = await _stream
.ReadAsync(trailer, _z.AvailableBytesIn, bytesNeeded, cancellationToken)
.ConfigureAwait(false);
if (
!await _stream
.ReadFullyAsync(
trailer,
_z.AvailableBytesIn,
bytesNeeded,
cancellationToken
)
.ConfigureAwait(false)
)
{
throw new ZlibException("Unexpected end-of-file reading GZIP trailer.");
}
}
else
{
_z.InputBuffer.AsSpan(_z.NextIn, trailer.Length).CopyTo(trailer);
}
ValidateGzipTrailer(trailer);
}
else
{
@@ -504,6 +483,36 @@ internal class ZlibBaseStream : Stream, IStreamStack
}
}
private void ValidateGzipTrailer(ReadOnlySpan<byte> trailer)
{
var expectedCrc = BinaryPrimitives.ReadInt32LittleEndian(trailer);
var actualCrc = crc.Crc32Result;
if (actualCrc != expectedCrc)
{
throw new ZlibException(
String.Format(
Constants.DefaultCultureInfo,
"Bad CRC32 in GZIP stream. (actual({0:X8})!=expected({1:X8}))",
actualCrc,
expectedCrc
)
);
}
var expectedSize = BinaryPrimitives.ReadInt32LittleEndian(trailer.Slice(4));
var actualSize = unchecked((int)_z.TotalBytesOut);
if (actualSize != expectedSize)
{
throw new ZlibException(
String.Format(
Constants.DefaultCultureInfo,
"Bad size in GZIP stream. (actual({0})!=expected({1}))",
actualSize,
expectedSize
)
);
}
}
private void end()
{
if (z is null)
@@ -668,6 +677,7 @@ internal class ZlibBaseStream : Stream, IStreamStack
#endif
private bool nomoreinput;
private bool _readCompleted;
private bool isDisposed;
private string ReadZeroTerminatedString()
@@ -736,7 +746,7 @@ internal class ZlibBaseStream : Stream, IStreamStack
return 0;
}
if (n != 10)
if (!_stream.ReadFully(header.Slice(n)))
{
throw new ZlibException("Not a valid GZIP stream.");
}
@@ -748,21 +758,23 @@ internal class ZlibBaseStream : Stream, IStreamStack
var timet = BinaryPrimitives.ReadInt32LittleEndian(header.Slice(4));
_GzipMtime = TarHeader.EPOCH.AddSeconds(timet);
totalBytesRead += n;
totalBytesRead += header.Length;
if ((header[3] & 0x04) == 0x04)
{
// read and discard extra field
n = _stream.Read(header.Slice(0, 2)); // 2-byte length field
totalBytesRead += n;
var extraLength = header[0] + (header[1] * 256);
var extra = new byte[extraLength];
n = _stream.Read(extra, 0, extra.Length);
if (n != extraLength)
if (!_stream.ReadFully(header.Slice(0, 2)))
{
throw new ZlibException("Unexpected end-of-file reading GZIP header.");
}
totalBytesRead += n;
totalBytesRead += 2;
var extraLength = header[0] + (header[1] * 256);
var extra = new byte[extraLength];
if (!_stream.ReadFully(extra))
{
throw new ZlibException("Unexpected end-of-file reading GZIP header.");
}
totalBytesRead += extraLength;
}
if ((header[3] & 0x08) == 0x08)
{
@@ -774,7 +786,12 @@ internal class ZlibBaseStream : Stream, IStreamStack
}
if ((header[3] & 0x02) == 0x02)
{
Read(_buf1, 0, 1); // CRC16, ignore
Span<byte> headerCrc = stackalloc byte[2];
if (!_stream.ReadFully(headerCrc))
{
throw new ZlibException("Unexpected end-of-file reading GZIP header CRC.");
}
totalBytesRead += 2;
}
return totalBytesRead;
@@ -796,7 +813,11 @@ internal class ZlibBaseStream : Stream, IStreamStack
return 0;
}
if (n != 10)
if (
!await _stream
.ReadFullyAsync(header, n, header.Length - n, cancellationToken)
.ConfigureAwait(false)
)
{
throw new ZlibException("Not a valid GZIP stream.");
}
@@ -808,23 +829,25 @@ internal class ZlibBaseStream : Stream, IStreamStack
var timet = BinaryPrimitives.ReadInt32LittleEndian(header.AsSpan(4));
_GzipMtime = TarHeader.EPOCH.AddSeconds(timet);
totalBytesRead += n;
totalBytesRead += header.Length;
if ((header[3] & 0x04) == 0x04)
{
// read and discard extra field
n = await _stream.ReadAsync(header, 0, 2, cancellationToken).ConfigureAwait(false); // 2-byte length field
totalBytesRead += n;
var extraLength = header[0] + (header[1] * 256);
var extra = new byte[extraLength];
n = await _stream
.ReadAsync(extra, 0, extra.Length, cancellationToken)
.ConfigureAwait(false);
if (n != extraLength)
if (
!await _stream.ReadFullyAsync(header, 0, 2, cancellationToken).ConfigureAwait(false)
)
{
throw new ZlibException("Unexpected end-of-file reading GZIP header.");
}
totalBytesRead += n;
totalBytesRead += 2;
var extraLength = header[0] + (header[1] * 256);
var extra = new byte[extraLength];
if (!await _stream.ReadFullyAsync(extra, cancellationToken).ConfigureAwait(false))
{
throw new ZlibException("Unexpected end-of-file reading GZIP header.");
}
totalBytesRead += extraLength;
}
if ((header[3] & 0x08) == 0x08)
{
@@ -838,7 +861,12 @@ internal class ZlibBaseStream : Stream, IStreamStack
}
if ((header[3] & 0x02) == 0x02)
{
await _stream.ReadAsync(_buf1, 0, 1, cancellationToken).ConfigureAwait(false); // CRC16, ignore
var headerCrc = new byte[2];
if (!await _stream.ReadFullyAsync(headerCrc, cancellationToken).ConfigureAwait(false))
{
throw new ZlibException("Unexpected end-of-file reading GZIP header CRC.");
}
totalBytesRead += 2;
}
return totalBytesRead;
@@ -889,7 +917,7 @@ internal class ZlibBaseStream : Stream, IStreamStack
_z.NextOut = offset;
_z.AvailableBytesOut = count;
if (count == 0)
if (count == 0 || _readCompleted)
{
return 0;
}
@@ -1012,6 +1040,7 @@ internal class ZlibBaseStream : Stream, IStreamStack
}
}
_readCompleted = !_wantCompress && rc == ZlibConstants.Z_STREAM_END;
rc = (count - _z.AvailableBytesOut);
// calculate CRC after reading
@@ -1020,12 +1049,8 @@ internal class ZlibBaseStream : Stream, IStreamStack
crc.SlurpBlock(buffer, offset, rc);
}
if (rc == ZlibConstants.Z_STREAM_END && z.AvailableBytesIn != 0 && !_wantCompress)
{
//rewind the buffer
this.Rewind(z.AvailableBytesIn);
z.AvailableBytesIn = 0;
}
// Preserve read-ahead bytes for trailer validation or the entry stream's Flush.
// rc is now an output byte count, not a codec status.
return rc;
}
@@ -1081,7 +1106,7 @@ internal class ZlibBaseStream : Stream, IStreamStack
_z.NextOut = offset;
_z.AvailableBytesOut = count;
if (count == 0)
if (count == 0 || _readCompleted)
{
return 0;
}
@@ -1206,6 +1231,7 @@ internal class ZlibBaseStream : Stream, IStreamStack
}
}
_readCompleted = !_wantCompress && rc == ZlibConstants.Z_STREAM_END;
rc = (count - _z.AvailableBytesOut);
// calculate CRC after reading
@@ -1214,12 +1240,7 @@ internal class ZlibBaseStream : Stream, IStreamStack
crc.SlurpBlock(buffer, offset, rc);
}
if (rc == ZlibConstants.Z_STREAM_END && z.AvailableBytesIn != 0 && !_wantCompress)
{
//rewind the buffer
this.Rewind(z.AvailableBytesIn);
z.AvailableBytesIn = 0;
}
// Preserve read-ahead bytes, as in the synchronous path.
return rc;
}

View File

@@ -24,7 +24,7 @@ internal sealed partial class AesDecoderStream : DecoderStream2
public AesDecoderStream(Stream input, byte[] info, IPasswordProvider pass, long limit)
{
var password = pass.CryptoGetTextPassword();
if (password == null)
if (password is null)
{
throw new SharpCompress.Common.CryptographicException(
"Encrypted 7Zip archive has no password specified."
@@ -43,7 +43,7 @@ internal sealed partial class AesDecoderStream : DecoderStream2
var passwordBytes = Encoding.Unicode.GetBytes(password);
var key = InitKey(numCyclesPower, salt, passwordBytes);
if (key == null)
if (key is null)
{
throw new ArchiveOperationException("Initialized with null key");
}
@@ -147,13 +147,24 @@ internal sealed partial class AesDecoderStream : DecoderStream2
private void Init(byte[] info, out int numCyclesPower, out byte[] salt, out byte[] iv)
{
if (info.Length == 0)
{
throw new InvalidFormatException("Missing 7z AES properties.");
}
var bt = info[0];
numCyclesPower = bt & 0x3F;
// Check before the no-salt/no-IV return, and before starting expensive key derivation.
// 0x3F is the format's direct-key mode, not an iteration exponent.
if (numCyclesPower > 24 && numCyclesPower != 0x3F)
{
throw new NotSupportedException("Unsupported 7z AES KDF count.");
}
if ((bt & 0xC0) == 0)
{
salt = Array.Empty<byte>();
iv = Array.Empty<byte>();
iv = new byte[16];
return;
}
@@ -183,11 +194,6 @@ internal sealed partial class AesDecoderStream : DecoderStream2
{
iv[i] = info[i + saltSize + 2];
}
if (numCyclesPower > 24)
{
throw new NotSupportedException();
}
}
private byte[]? InitKey(int mNumCyclesPower, byte[] salt, byte[] pass)

View File

@@ -303,14 +303,23 @@ internal static class DecoderStreamHelper
out var primaryCoderIndex,
out var primaryOutStreamIndex
);
return CreateDecoderStream(
inStreams,
packSizes,
outStreams,
folderInfo,
primaryCoderIndex,
pass
);
var encrypted = folderInfo.IsEncrypted();
try
{
var decoder = CreateDecoderStream(
inStreams,
packSizes,
outStreams,
folderInfo,
primaryCoderIndex,
pass
);
return encrypted ? new EncryptedDecoderStream(decoder) : decoder;
}
catch (DataErrorException ex) when (encrypted)
{
throw EncryptedDecoderStream.DecodingException(ex);
}
}
internal static async ValueTask<Stream> CreateDecoderStreamAsync(
@@ -341,15 +350,24 @@ internal static class DecoderStreamHelper
out var primaryCoderIndex,
out var primaryOutStreamIndex
);
return await CreateDecoderStreamAsync(
inStreams,
packSizes,
outStreams,
folderInfo,
primaryCoderIndex,
pass,
cancellationToken
)
.ConfigureAwait(false);
var encrypted = folderInfo.IsEncrypted();
try
{
var decoder = await CreateDecoderStreamAsync(
inStreams,
packSizes,
outStreams,
folderInfo,
primaryCoderIndex,
pass,
cancellationToken
)
.ConfigureAwait(false);
return encrypted ? new EncryptedDecoderStream(decoder) : decoder;
}
catch (DataErrorException ex) when (encrypted)
{
throw EncryptedDecoderStream.DecodingException(ex);
}
}
}

View File

@@ -0,0 +1,58 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
namespace SharpCompress.Compressors.LZMA;
// AES has no password verifier. Decoder errors can mean either a wrong password
// or damaged ciphertext; retain the codec error without claiming certainty.
internal sealed class EncryptedDecoderStream(Stream decoder) : DecoderStream2
{
internal static CryptographicException DecodingException(Exception exception) =>
new("Incorrect password or corrupt encrypted 7z data.", exception);
public override long Length => decoder.Length;
public override long Position => decoder.Position;
public override int Read(byte[] buffer, int offset, int count)
{
try
{
return decoder.Read(buffer, offset, count);
}
catch (DataErrorException ex)
{
throw DecodingException(ex);
}
}
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
try
{
return await decoder
.ReadAsync(buffer, offset, count, cancellationToken)
.ConfigureAwait(false);
}
catch (DataErrorException ex)
{
throw DecodingException(ex);
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
decoder.Dispose();
}
base.Dispose(disposing);
}
}

View File

@@ -129,11 +129,7 @@ public sealed partial class LZipStream
var header = ArrayPool<byte>.Shared.Rent(6);
try
{
var n = await stream.ReadAsync(header, 0, 6, cancellationToken).ConfigureAwait(false);
// TODO: Handle reading only part of the header?
if (n != 6)
if (!await stream.ReadFullyAsync(header, 0, 6, cancellationToken).ConfigureAwait(false))
{
return 0;
}
@@ -148,13 +144,7 @@ public sealed partial class LZipStream
{
return 0;
}
var basePower = header[5] & 0x1F;
var subtractionNumerator = (header[5] & 0xE0) >> 5;
if (basePower < 4 || basePower > 30)
{
return 0;
}
return (1 << basePower) - (subtractionNumerator * (1 << (basePower - 4)));
return DecodeDictionarySize(header[5]);
}
finally
{

View File

@@ -253,11 +253,7 @@ public sealed partial class LZipStream : Stream, IFinishable
{
// Read the header
Span<byte> header = stackalloc byte[6];
var n = stream.Read(header);
// TODO: Handle reading only part of the header?
if (n != 6)
if (!stream.ReadFully(header))
{
return 0;
}
@@ -272,9 +268,18 @@ public sealed partial class LZipStream : Stream, IFinishable
{
return 0;
}
var basePower = header[5] & 0x1F;
var subtractionNumerator = (header[5] & 0xE0) >> 5;
return (1 << basePower) - (subtractionNumerator * (1 << (basePower - 4)));
return DecodeDictionarySize(header[5]);
}
private static int DecodeDictionarySize(byte encoded)
{
var basePower = encoded & 0x1F;
if (basePower < 12 || basePower > 29)
{
return 0;
}
var size = (1 << basePower) - ((encoded >> 5) * (1 << (basePower - 4)));
return size >= 4096 ? size : 0;
}
// Async methods moved to LZipStream.Async.cs

View File

@@ -0,0 +1,21 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Ace.Headers;
using SharpCompress.Test.Mocks;
using Xunit;
namespace SharpCompress.Test.Ace;
public class AceHeaderReadTests
{
[Fact]
public async Task Ace_Signature_Allows_Partial_Reads()
{
byte[] header = [0, 0, 0, 0, 0, 0, 0, 42, 42, 65, 67, 69, 42, 42];
using var sync = new ShortReadStream(new MemoryStream(header));
using var asyncInput = new ShortReadStream(new MemoryStream(header));
Assert.True(AceHeader.IsArchive(sync));
Assert.True(await AceHeader.IsArchiveAsync(asyncInput, CancellationToken.None));
}
}

View File

@@ -0,0 +1,26 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Arc;
using SharpCompress.Test.Mocks;
using Xunit;
namespace SharpCompress.Test.Arc;
public class ArcHeaderReadTests
{
[Fact]
public async Task Arc_Header_Allows_Partial_Reads()
{
var header = new byte[29];
header[0] = 0x1A;
header[1] = 2;
header[2] = (byte)'x';
using var sync = new ShortReadStream(new MemoryStream(header));
using var asyncInput = new ShortReadStream(new MemoryStream(header));
var parser = new ArcEntryHeader(new ArchiveEncoding());
Assert.Equal("x", parser.ReadHeader(sync)!.Name);
Assert.Equal("x", (await parser.ReadHeaderAsync(asyncInput, CancellationToken.None))!.Name);
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Arj.Headers;
using SharpCompress.Crypto;
using SharpCompress.Test.Mocks;
using Xunit;
namespace SharpCompress.Test.Arj;
public class ArjHeaderReadTests
{
[Fact]
public async Task Arj_Header_Allows_Partial_Reads()
{
byte[] header = [0x60, 0xEA, 1, 0, 42, 0, 0, 0, 0];
BitConverter.GetBytes(Crc32Stream.Compute(new byte[] { 42 })).CopyTo(header, 5);
var parser = new ArjMainHeader(new ArchiveEncoding());
using var sync = new ShortReadStream(new MemoryStream(header));
using var asyncInput = new ShortReadStream(new MemoryStream(header));
Assert.Equal(new byte[] { 42 }, parser.ReadHeader(sync));
Assert.Equal(
new byte[] { 42 },
await parser.ReadHeaderAsync(asyncInput, CancellationToken.None)
);
}
[Fact]
public async Task Arj_Extended_Header_Allows_Partial_Reads()
{
byte[] header = [1, 0, 42, 0, 0, 0, 0, 0, 0];
BitConverter.GetBytes(Crc32Stream.Compute(new byte[] { 42 })).CopyTo(header, 3);
using var sync = new ShortReadStream(new MemoryStream(header));
using var asyncInput = new ShortReadStream(new MemoryStream(header));
var parser = new ExtendedArjHeader();
Assert.Equal(new byte[] { 42 }, Assert.Single(parser.ReadExtended(sync)));
Assert.Equal(new byte[] { 42 }, Assert.Single(await parser.ReadExtendedAsync(asyncInput)));
}
private sealed class ExtendedArjHeader : ArjMainHeader
{
public ExtendedArjHeader()
: base(new ArchiveEncoding()) { }
public List<byte[]> ReadExtended(Stream input) => ReadExtendedHeaders(input);
public ValueTask<List<byte[]>> ReadExtendedAsync(Stream input) =>
ReadExtendedHeadersAsync(input, CancellationToken.None);
}
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Buffers.Binary;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.GZip;
@@ -15,6 +16,49 @@ namespace SharpCompress.Test.GZip;
public class GZipCrcExtractionTests : TestBase
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task GZipReader_Async_Only_NonSeekable_Validates_Fragmented_Trailer(bool corrupt)
{
var bytes = corrupt
? ReadCorruptedGZipTrailer(true)
: File.ReadAllBytes(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
using var fragmented = new ShortReadStream(
new MemoryStream(bytes),
maxReadSize: 7,
seekable: false
);
using var input = new AsyncOnlyStream(fragmented, disposeStream: false);
await using var reader = await GZipReader.OpenAsyncReader(
input,
new ReaderOptions { LeaveStreamOpen = true },
CancellationToken.None
);
Assert.True(await reader.MoveToNextEntryAsync(CancellationToken.None));
var destination = Path.Combine(SCRATCH_FILES_PATH, Guid.NewGuid().ToString());
if (corrupt)
{
await Assert.ThrowsAsync<InvalidFormatException>(async () =>
await reader.WriteEntryToFileAsync(
destination,
cancellationToken: CancellationToken.None
)
);
}
else
{
await reader.WriteEntryToFileAsync(
destination,
cancellationToken: CancellationToken.None
);
Assert.Equal(
File.ReadAllBytes(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")),
File.ReadAllBytes(destination)
);
}
}
[Fact]
public void GZipArchive_WriteToFile_Throws_On_Crc_Mismatch()
{

View File

@@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AwesomeAssertions;
using SharpCompress.Archives.GZip;
using SharpCompress.Compressors.Deflate;
using SharpCompress.Readers;
using SharpCompress.Readers.GZip;
using SharpCompress.Test.Mocks;
using Xunit;
@@ -17,6 +21,290 @@ public class GZipExtraFieldTests
{
private static readonly byte[] EXPECTED_PAYLOAD = [.. "gzip payload"u8];
[Theory]
[InlineData("Stream", false)]
[InlineData("Stream", true)]
[InlineData("Archive", false)]
[InlineData("Archive", true)]
[InlineData("Reader", false)]
[InlineData("Reader", true)]
public async Task GZip_Rejects_Truncated_Header_Crc(string api, bool async)
{
byte[] bytes = [0x1F, 0x8B, 8, 2, 0, 0, 0, 0, 0, 3, 0];
using var input = new ShortReadStream(new MemoryStream(bytes));
await Assert.ThrowsAsync<ZlibException>(() =>
ReadPayload(input, api, async, CancellationToken.None)
);
Assert.False(input.IsDisposed);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task GZip_Header_Crc_Field_Is_Two_Bytes(bool async)
{
var original = CreateGZipWithExtraField(0, null);
original[3] |= 2;
var headerCrc = SharpCompress.Crypto.Crc32Stream.Compute(original.AsSpan(0, 12).ToArray());
byte[] bytes =
[
.. original.AsSpan(0, 12),
(byte)headerCrc,
(byte)(headerCrc >> 8),
.. original.AsSpan(12),
];
using var input = new ShortReadStream(new MemoryStream(bytes));
using var output = new MemoryStream();
if (async)
{
await using var gzip = new SharpCompress.Compressors.Deflate.GZipStream(
input,
SharpCompressionMode.Decompress,
CompressionLevel.Default,
Encoding.UTF8,
leaveOpen: true
);
await gzip.CopyToAsync(output, 81920, CancellationToken.None);
}
else
{
using var gzip = new SharpCompress.Compressors.Deflate.GZipStream(
input,
SharpCompressionMode.Decompress,
CompressionLevel.Default,
Encoding.UTF8,
leaveOpen: true
);
gzip.CopyTo(output);
}
Assert.Equal(EXPECTED_PAYLOAD, output.ToArray());
using var archiveInput = new ShortReadStream(new MemoryStream(bytes));
if (async)
{
await using var archive = await GZipArchive.OpenAsyncArchive(archiveInput);
var entry = await archive.EntriesAsync.FirstAsync();
using var entryStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
output.SetLength(0);
await entryStream.CopyToAsync(output, 81920, CancellationToken.None);
}
else
{
using var archive = GZipArchive.OpenArchive(archiveInput);
using var entryStream = archive.Entries.Single().OpenEntryStream();
output.SetLength(0);
entryStream.CopyTo(output);
}
Assert.Equal(EXPECTED_PAYLOAD, output.ToArray());
}
[Theory]
[InlineData(0)]
[InlineData(4)]
public async Task GZip_Async_Dispose_Rejects_Corrupt_Trailer(int offset)
{
var bytes = CreateGZipWithExtraField(0, null);
bytes[bytes.Length - 8 + offset] ^= 1;
using var input = new ShortReadStream(new MemoryStream(bytes));
var gzip = new SharpCompress.Compressors.Deflate.GZipStream(
input,
SharpCompressionMode.Decompress,
CompressionLevel.Default,
Encoding.UTF8,
leaveOpen: true
);
using var output = new MemoryStream();
await gzip.CopyToAsync(output, 81920, CancellationToken.None);
await Assert.ThrowsAsync<SharpCompress.Compressors.Deflate.ZlibException>(async () =>
await gzip.DisposeAsync()
);
}
public static IEnumerable<object[]> FragmentedExtraFields()
{
foreach (var api in new[] { "Stream", "Reader", "Archive" })
{
foreach (var useAsync in new[] { false, true })
{
foreach (var seekable in new[] { false, true })
{
foreach (var extraLength in new[] { 0, 32768, 65535 })
{
foreach (var chunkSize in new[] { 1, 7, 1024 })
{
if (api != "Archive" || seekable)
{
yield return [api, useAsync, seekable, extraLength, chunkSize];
}
}
}
}
}
}
}
[Theory]
[MemberData(nameof(FragmentedExtraFields))]
public async Task GZip_Read_WithFragmentedExtraField_ShouldReturnPayload(
string api,
bool useAsync,
bool seekable,
int extraLength,
int chunkSize
)
{
using var input = new ShortReadStream(
new MemoryStream(CreateGZipWithExtraField(extraLength, "payload.bin")),
chunkSize,
seekable
);
using var asyncOnly = new AsyncOnlyStream(input, disposeStream: false);
Assert.Equal(
EXPECTED_PAYLOAD,
await ReadPayload(useAsync ? asyncOnly : input, api, useAsync, CancellationToken.None)
);
Assert.False(input.IsDisposed);
}
public static IEnumerable<object[]> TruncatedExtraFields()
{
foreach (var api in new[] { "Stream", "Reader", "Archive" })
{
foreach (var useAsync in new[] { false, true })
{
foreach (var length in new[] { 1, 9, 10, 11, 12, 13, 43 })
{
yield return [api, useAsync, length];
}
}
}
}
[Theory]
[MemberData(nameof(TruncatedExtraFields))]
public async Task GZip_Read_RejectsTruncatedExtraField(string api, bool useAsync, int length)
{
using var input = new ShortReadStream(
new MemoryStream(CreateGZipWithExtraField(32).Take(length).ToArray())
);
await Assert.ThrowsAsync<ZlibException>(() =>
ReadPayload(input, api, useAsync, CancellationToken.None)
);
Assert.False(input.IsDisposed);
}
[Theory]
[InlineData(false, 1)]
[InlineData(true, 1)]
[InlineData(false, 7)]
[InlineData(true, 7)]
[InlineData(false, 8)]
[InlineData(true, 8)]
public async Task GZipStream_Read_RejectsTruncatedTrailer(bool useAsync, int missingBytes)
{
var data = CreateGZipWithExtraField(32);
using var input = new ShortReadStream(
new MemoryStream(data.Take(data.Length - missingBytes).ToArray())
);
await Assert.ThrowsAsync<ZlibException>(() =>
ReadPayload(input, "Stream", useAsync, CancellationToken.None)
);
Assert.False(input.IsDisposed);
}
[Theory]
[InlineData("Stream")]
[InlineData("Reader")]
[InlineData("Archive")]
public async Task GZip_ReadAsync_WithExtraField_HonorsCancellation(string api)
{
using var input = new ShortReadStream(new MemoryStream(CreateGZipWithExtraField(65535)));
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
ReadPayload(input, api, true, cancellation.Token)
);
Assert.False(input.IsDisposed);
}
private static async Task<byte[]> ReadPayload(
Stream input,
string api,
bool useAsync,
CancellationToken cancellationToken
)
{
var options = ReaderOptions.ForExternalStream;
using var output = new MemoryStream();
switch (api)
{
case "Stream":
if (useAsync)
{
await using var gzip = new SharpCompress.Compressors.Deflate.GZipStream(
input,
SharpCompressionMode.Decompress,
options
);
await gzip.CopyToAsync(output, 81920, cancellationToken);
}
else
{
using var gzip = new SharpCompress.Compressors.Deflate.GZipStream(
input,
SharpCompressionMode.Decompress,
options
);
gzip.CopyTo(output);
}
break;
case "Reader":
if (useAsync)
{
await using var reader = await GZipReader.OpenAsyncReader(
input,
options,
cancellationToken
);
Assert.True(await reader.MoveToNextEntryAsync(cancellationToken));
await reader.WriteEntryToAsync(output, cancellationToken);
}
else
{
using var reader = GZipReader.OpenReader(input, options);
Assert.True(reader.MoveToNextEntry());
reader.WriteEntryTo(output);
}
break;
case "Archive":
if (useAsync)
{
await using var archive = await GZipArchive.OpenAsyncArchive(
input,
options,
cancellationToken
);
await foreach (
var entry in archive.EntriesAsync.WithCancellation(cancellationToken)
)
{
using var entryStream = await entry.OpenEntryStreamAsync(cancellationToken);
await entryStream.CopyToAsync(output, 81920, cancellationToken);
break;
}
}
else
{
using var archive = GZipArchive.OpenArchive(input, options);
using var entryStream = archive.Entries.Single().OpenEntryStream();
entryStream.CopyTo(output);
}
break;
default:
throw new ArgumentException("Unknown GZip API", nameof(api));
}
return output.ToArray();
}
[Theory]
[InlineData(32767)]
[InlineData(32768)]

View File

@@ -0,0 +1,45 @@
using System;
using System.Buffers.Binary;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.GZip;
using SharpCompress.Crypto;
using SharpCompress.Test.Mocks;
using Xunit;
namespace SharpCompress.Test.GZip;
public class GZipTrailerReadTests
{
[Theory]
[InlineData(8)]
[InlineData(7)]
public async Task GZip_Async_Validation_Reads_Fragmented_Trailer(int length)
{
byte[] payload = [1, 2, 3];
var trailer = new byte[8];
BinaryPrimitives.WriteUInt32LittleEndian(trailer, Crc32Stream.Compute(payload));
BinaryPrimitives.WriteUInt32LittleEndian(trailer.AsSpan(4), 3);
using var input = new MemoryStream(payload);
using var fragmented = new ShortReadStream(
new MemoryStream(trailer.AsSpan(0, length).ToArray()),
seekable: false
);
using var raw = new AsyncOnlyStream(fragmented, disposeStream: false);
using var stream = new GZipChecksumValidationStream(input, raw, "test", null, null);
using var output = new MemoryStream();
if (length == 8)
{
await stream.CopyToAsync(output, 81920, CancellationToken.None);
Assert.Equal(payload, output.ToArray());
}
else
{
await Assert.ThrowsAsync<IncompleteArchiveException>(async () =>
await stream.CopyToAsync(output, 81920, CancellationToken.None)
);
}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Test.Mocks;
// Exercise legal partial reads through every stream read overload used by the parsers.
internal sealed class ShortReadStream(Stream stream, int maxReadSize = 1, bool seekable = true)
: TestStream(stream, read: true, write: false, seek: seekable)
{
public override int Read(byte[] buffer, int offset, int count) =>
base.Read(buffer, offset, Math.Min(count, maxReadSize));
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
) => base.ReadAsync(buffer, offset, Math.Min(count, maxReadSize), cancellationToken);
#if !LEGACY_DOTNET
public override ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
) => base.ReadAsync(buffer.Slice(0, Math.Min(buffer.Length, maxReadSize)), cancellationToken);
#endif
}

View File

@@ -1,5 +1,6 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Common.Rar;
using SharpCompress.Common.Rar.Headers;
using SharpCompress.IO;
@@ -10,6 +11,76 @@ namespace SharpCompress.Test.Rar;
public class CryptKey5Tests : TestBase
{
[Theory]
[InlineData(0)]
[InlineData(24)]
[InlineData(25)]
[InlineData(30)]
[InlineData(31)]
[InlineData(32)]
[InlineData(36)]
[InlineData(128)]
[InlineData(255)]
public async Task Rar5_CryptoInfo_Validates_Kdf_Count(int lg2Count)
{
// The KDF count is one raw byte, not a vint. Test the upper bound without deriving a key.
var data = new byte[35];
data[2] = (byte)lg2Count;
using var syncStream = new MemoryStream(data);
using var asyncStream = new MemoryStream(data);
using var syncReader = new MarkingBinaryReader(syncStream);
using var asyncReader = new AsyncMarkingBinaryReader(asyncStream);
if (lg2Count <= 24)
{
Assert.Equal(lg2Count, Rar5CryptoInfo.Create(syncReader, true).LG2Count);
Assert.Equal(lg2Count, (await Rar5CryptoInfo.CreateAsync(asyncReader, true)).LG2Count);
}
else
{
Assert.Throws<SharpCompress.Common.CryptographicException>(() =>
Rar5CryptoInfo.Create(syncReader, true)
);
await Assert.ThrowsAsync<SharpCompress.Common.CryptographicException>(async () =>
await Rar5CryptoInfo.CreateAsync(asyncReader, true)
);
Assert.Equal(3, syncStream.Position);
Assert.Equal(3, asyncStream.Position);
}
}
[Theory]
[InlineData(-1)]
[InlineData(25)]
[InlineData(30)]
[InlineData(31)]
[InlineData(32)]
[InlineData(33)]
[InlineData(34)]
[InlineData(35)]
[InlineData(36)]
[InlineData(255)]
[InlineData(int.MaxValue)]
public void Rar5_KeyCache_Rejects_Invalid_Kdf_Count(int lg2Count)
{
var cache = new Rar5KeyCache();
Assert.Throws<SharpCompress.Common.CryptographicException>(() =>
cache.GetKeys("test", new byte[16], lg2Count)
);
}
[Fact]
public void Rar5_Key_Rejects_Mutated_Kdf_Count()
{
var info = ReadCryptoInfo();
var key = new CryptKey5("test", info);
using var transform = key.Transformer(info.Salt);
info.LG2Count = 32;
Assert.Throws<SharpCompress.Common.CryptographicException>(() =>
key.Transformer(info.Salt)
);
}
private Rar5CryptoInfo ReadCryptoInfo()
{
using var stream = File.OpenRead(

View File

@@ -9,6 +9,7 @@ using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Common;
using SharpCompress.Common.Rar;
using SharpCompress.Common.Rar.Headers;
using SharpCompress.Compressors.Rar;
using SharpCompress.IO;
using SharpCompress.Readers;
@@ -203,7 +204,52 @@ public class RarCommentTests : TestBase
Assert.True(stream.CanRead);
}
public static IEnumerable<object[]> UnsupportedCommentMethods()
public static IEnumerable<object[]> TruncatedEncryptedComments()
{
foreach (var useAsync in new[] { false, true })
{
foreach (var useReader in new[] { false, true })
{
foreach (var length in new[] { 0, 15, 16, 31 })
{
yield return [useAsync, useReader, length];
}
}
}
}
[Theory]
[MemberData(nameof(TruncatedEncryptedComments))]
public async Task Rar5_Comment_Rejects_Truncated_Encrypted_Data(
bool useAsync,
bool useReader,
int length
)
{
var data = File.ReadAllBytes(
Path.Combine(TEST_ARCHIVES_PATH, "Rar5.comment.encrypted17.rar")
);
using var source = new MemoryStream(data);
var factory = new RarHeaderFactory(
StreamingMode.Seekable,
ReaderOptions.ForExternalStream.WithPassword("test")
);
var comment = factory
.ReadHeaders(source)
.OfType<FileHeader>()
.First(x => x.FileName == "CMT");
using var packed = comment.PackedStream!;
Assert.NotNull(comment.Rar5CryptoInfo);
Assert.True(comment.CompressedSize > length);
using var truncated = new MemoryStream(data.Take((int)source.Position + length).ToArray());
await Assert.ThrowsAsync<InvalidFormatException>(() =>
ReadCommentArchive(truncated, useAsync, useReader, "test")
);
Assert.True(truncated.CanRead);
}
public static IEnumerable<object[]> NonStoredCommentMethods()
{
foreach (var useAsync in new[] { false, true })
{
@@ -218,8 +264,12 @@ public class RarCommentTests : TestBase
}
[Theory]
[MemberData(nameof(UnsupportedCommentMethods))]
public async Task Rar5_Comment_Skips_Nonzero_Methods(int method, bool useAsync, bool useReader)
[MemberData(nameof(NonStoredCommentMethods))]
public async Task Rar5_Comment_Skips_NonStored_Methods(
int method,
bool useAsync,
bool useReader
)
{
// Ignored comments must not allocate from their unpacked size or disturb the next entry.
using var stream = CreateCommentArchive(int.MaxValue, method, [1, 2, 3]);
@@ -230,10 +280,11 @@ public class RarCommentTests : TestBase
private static async Task<string?> ReadCommentArchive(
Stream stream,
bool useAsync,
bool useReader
bool useReader,
string? password = null
)
{
var options = ReaderOptions.ForExternalStream;
var options = ReaderOptions.ForExternalStream with { Password = password };
if (useReader)
{
using var forwardOnly = new ForwardOnlyStream(

View File

@@ -0,0 +1,120 @@
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Rar;
using SharpCompress.IO;
using SharpCompress.Test.Mocks;
using Xunit;
namespace SharpCompress.Test.Rar;
public class RarCryptoWrapperTests
{
private static readonly byte[] Plaintext = Enumerable
.Range(0, 32)
.Select(x => (byte)x)
.ToArray();
[Theory]
[InlineData(false, 1)]
[InlineData(true, 1)]
[InlineData(false, 7)]
[InlineData(true, 7)]
[InlineData(false, 17)]
[InlineData(true, 17)]
[InlineData(false, 32)]
[InlineData(true, 32)]
public async Task Rar5_Crypto_Reads_Fragmented_Blocks_And_Buffered_Plaintext(
bool useAsync,
int readSize
)
{
var info = CreateCryptoInfo();
using var input = new ShortReadStream(new MemoryStream(Encrypt(info)));
using var asyncOnly = new AsyncOnlyStream(input, disposeStream: false);
using var crypto = new RarCryptoWrapper(
useAsync ? asyncOnly : input,
info.Salt,
new CryptKey5("test", info)
);
var result = new byte[Plaintext.Length];
var offset = 0;
while (offset < result.Length)
{
var count = Math.Min(readSize, result.Length - offset);
var read = useAsync
? await crypto.ReadAsync(result, offset, count, CancellationToken.None)
: crypto.Read(result, offset, count);
Assert.Equal(count, read);
offset += read;
}
Assert.Equal(Plaintext, result);
Assert.Equal(0, crypto.Read(result, 0, 0));
Assert.False(input.IsDisposed);
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(15)]
[InlineData(16)]
[InlineData(17)]
[InlineData(31)]
public async Task Rar5_Crypto_Rejects_Truncated_Ciphertext(int length)
{
var info = CreateCryptoInfo();
var data = Encrypt(info).Take(length).ToArray();
using var syncInput = new ShortReadStream(new MemoryStream(data));
using var asyncInput = new ShortReadStream(new MemoryStream(data));
using var syncCrypto = new RarCryptoWrapper(
syncInput,
info.Salt,
new CryptKey5("test", info)
);
using var asyncCrypto = new RarCryptoWrapper(
asyncInput,
info.Salt,
new CryptKey5("test", info)
);
Assert.Throws<InvalidFormatException>(() => syncCrypto.Read(new byte[32], 0, 32));
await Assert.ThrowsAsync<InvalidFormatException>(async () =>
await asyncCrypto.ReadAsync(new byte[32], 0, 32, CancellationToken.None)
);
}
[Fact]
public async Task Rar5_Crypto_Async_Cancellation()
{
var info = CreateCryptoInfo();
using var input = new MemoryStream(Encrypt(info));
using var crypto = new RarCryptoWrapper(input, info.Salt, new CryptKey5("test", info));
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
await crypto.ReadAsync(new byte[32], 0, 32, cancellation.Token)
);
Assert.Equal(0, input.Position);
}
private static Rar5CryptoInfo CreateCryptoInfo()
{
// Use a one-iteration KDF so these stream tests do not spend time deriving keys.
using var stream = new MemoryStream(new byte[35]);
using var reader = new MarkingBinaryReader(stream);
return Rar5CryptoInfo.Create(reader, true);
}
private static byte[] Encrypt(Rar5CryptoInfo info)
{
using var aes = Aes.Create();
aes.Key = CryptKey5.GenerateRarPBKDF2Key("test", info.Salt, 1)[0];
aes.IV = info.InitV;
aes.Padding = PaddingMode.None;
using var encryptor = aes.CreateEncryptor();
return encryptor.TransformFinalBlock(Plaintext, 0, Plaintext.Length);
}
}

View File

@@ -81,7 +81,7 @@ public class RarReaderAsyncTests : ReaderTests
[Fact]
public async ValueTask Rar_Multi_Reader_Encrypted_Async() =>
await Assert.ThrowsAsync<IncompleteArchiveException>(async () =>
await Assert.ThrowsAsync<InvalidFormatException>(async () =>
{
string[] archives =
[

View File

@@ -0,0 +1,45 @@
using System;
using System.IO;
using SharpCompress.Archives.Rar;
using SharpCompress.Common;
using Xunit;
namespace SharpCompress.Test.Rar;
public class RarVolumeCasingTests : TestBase
{
[Theory]
[InlineData("sample.RAR", "SAMPLE.R00")]
[InlineData("sample.part01.RAR", "SAMPLE.PART02.rar")]
[InlineData("sample.rar.001", "SAMPLE.RAR.002")]
public void Companion_Lookup_Is_Case_Insensitive(string firstName, string nextName)
{
var first = new FileInfo(Path.Combine(SCRATCH_FILES_PATH, firstName));
var next = Path.Combine(SCRATCH_FILES_PATH, nextName);
File.WriteAllBytes(next, []);
Assert.Equal(
next,
RarArchiveVolumeFactory.GetFilePart(1, first)!.FullName,
ignoreCase: true
);
Assert.Null(RarArchiveVolumeFactory.GetFilePart(2, first));
}
[Fact]
public void Companion_Lookup_Prefers_Exact_Path_And_Rejects_Ambiguity()
{
var first = new FileInfo(Path.Combine(SCRATCH_FILES_PATH, "sample.rar"));
var upper = Path.Combine(SCRATCH_FILES_PATH, "SAMPLE.R00");
var mixed = Path.Combine(SCRATCH_FILES_PATH, "Sample.r00");
File.WriteAllBytes(upper, []);
if (File.Exists(mixed))
{
return; // Case-insensitive filesystems cannot represent ambiguous companion names.
}
File.WriteAllBytes(mixed, []);
Assert.Throws<InvalidFormatException>(() => RarArchiveVolumeFactory.GetFilePart(1, first));
var exact = Path.Combine(SCRATCH_FILES_PATH, "sample.r00");
File.WriteAllBytes(exact, []);
Assert.Equal(exact, RarArchiveVolumeFactory.GetFilePart(1, first)!.FullName);
}
}

View File

@@ -0,0 +1,78 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Compressors.LZMA;
using Xunit;
namespace SharpCompress.Test.SevenZip;
public class EncryptedDecoderStreamTests
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task EncryptedDecoderStream_DataError_PreservesInnerException(bool async)
{
var error = new DataErrorException();
using var decoder = new FailingStream(error);
using var stream = new EncryptedDecoderStream(decoder);
var exception = await Assert.ThrowsAsync<CryptographicException>(async () =>
{
if (async)
{
await stream.ReadAsync(new byte[1], 0, 1, CancellationToken.None);
}
else
{
stream.Read(new byte[1], 0, 1);
}
});
Assert.Same(error, exception.InnerException);
}
[Fact]
public async Task EncryptedDecoderStream_OtherErrors_AreNotTranslated()
{
var error = new IOException("read failed");
using var decoder = new FailingStream(error);
using var stream = new EncryptedDecoderStream(decoder);
Assert.Same(error, Assert.Throws<IOException>(() => stream.ReadByte()));
Assert.Same(
error,
await Assert.ThrowsAsync<IOException>(() => stream.ReadAsync(new byte[1], 0, 1))
);
using var canceled = new CancellationTokenSource();
canceled.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
stream.ReadAsync(new byte[1], 0, 1, canceled.Token)
);
stream.Dispose();
Assert.True(decoder.Disposed);
}
private sealed class FailingStream(Exception exception) : MemoryStream
{
public bool Disposed { get; private set; }
public override int Read(byte[] buffer, int offset, int count) => throw exception;
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
cancellationToken.ThrowIfCancellationRequested();
throw exception;
}
protected override void Dispose(bool disposing)
{
Disposed = true;
base.Dispose(disposing);
}
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.IO;
using SharpCompress.Compressors.LZMA;
using SharpCompress.Compressors.LZMA.Utilities;
using Xunit;
namespace SharpCompress.Test.SevenZip;
public class SevenZipAesKdfTests
{
[Theory]
[InlineData(25)]
[InlineData(31)]
[InlineData(62)]
public void SevenZipAes_Rejects_Excessive_Kdf_Without_Salt(int exponent)
{
using var input = new MemoryStream();
Assert.Throws<NotSupportedException>(() =>
new AesDecoderStream(input, [(byte)exponent], new Password(), 0)
);
}
[Fact]
public void SevenZipAes_Allows_Direct_Key_Without_Iv()
{
using var input = new MemoryStream();
using var decoder = new AesDecoderStream(input, [0x3F], new Password(), 0);
Assert.Equal(0, decoder.Read(new byte[1], 0, 1));
}
private sealed class Password : IPasswordProvider
{
public string CryptoGetTextPassword() => "test";
}
}

View File

@@ -0,0 +1,82 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Common.SevenZip;
using SharpCompress.Crypto;
using SharpCompress.Writers.SevenZip;
using Xunit;
namespace SharpCompress.Test.SevenZip;
public class SevenZipEncryptionDetectionTests
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task SevenZipArchive_EmptyFirstFile_EncryptionDetection(bool encrypted)
{
using var output = new MemoryStream();
using (var writer = new SevenZipWriter(output, new SevenZipWriterOptions()))
{
using var empty = new MemoryStream();
using var data = new MemoryStream(new byte[] { 1, 2, 3 });
writer.Write("empty.txt", empty, null);
writer.Write("data.txt", data, null);
}
output.Position = 0;
using var archive = (SevenZipArchive)SevenZipArchive.OpenArchive(output);
var entries = archive.Entries.Cast<SevenZipArchiveEntry>().ToList();
if (encrypted)
{
// Only metadata is under test; do not decode after adding the AES marker.
entries[1]
.FilePart.Folder!._coders.Add(new CCoderInfo { _methodId = CMethodId.K_AES });
}
Assert.False(entries[0].IsEncrypted);
Assert.Same(Stream.Null, entries[0].OpenEntryStream());
Assert.Equal(encrypted, entries[1].IsEncrypted);
Assert.Equal(encrypted, archive.IsEncrypted);
Assert.Equal(encrypted, await archive.IsEncryptedAsync());
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void SevenZipArchive_NoFiles_IsNotEncrypted(bool directory)
{
using var stream = new MemoryStream();
// Build reader fixtures directly so this PR does not depend on the writer fix in #1440.
using var header = new MemoryStream();
ArchiveHeaderWriter.WriteRawHeader(
header,
null,
new SevenZipFilesInfoWriter
{
Entries = directory
? new[]
{
new SevenZipWriteEntry
{
Name = "dir",
IsDirectory = true,
IsEmpty = true,
Attributes = 0x10,
},
}
: System.Array.Empty<SevenZipWriteEntry>(),
}
);
var bytes = header.ToArray();
var crc = Crc32Stream.Compute(
Crc32Stream.DEFAULT_POLYNOMIAL,
Crc32Stream.DEFAULT_SEED,
bytes
);
SevenZipSignatureHeaderWriter.WriteFinal(stream, 0, (ulong)bytes.Length, crc);
stream.Write(bytes, 0, bytes.Length);
stream.Position = 0;
using var archive = SevenZipArchive.OpenArchive(stream);
Assert.False(archive.IsEncrypted);
}
}

View File

@@ -0,0 +1,113 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Common;
using SharpCompress.Compressors.LZMA;
using SharpCompress.Readers;
using Xunit;
namespace SharpCompress.Test.SevenZip;
public class SevenZipPasswordExceptionTests : ArchiveTests
{
// 206-byte fixture generated with 7zz a -psecret -mhe=on -m0=LZMA2
// encrypted-header.7z data.txt (data.txt contains "hello"). No external tool needed in CI.
private const string EncryptedHeader =
"N3q8ryccAAQx1ES9gAAAAAAAAAAuAAAAAAAAADjCO/gWFzzAowVAn1/zBdIMi+5r2AyhxUL1SKmLFzNxH6VJqIWJTTzQDTAhcddAjSIgK/e92hRdPfRIZYJaEPZcue3MFygwZTMOKvgnqcH5B+k/o2345MpVIuA4n2yg4ag99CRyj4LF52P188ujA4nEMXCEsl5lKfQmln5QFOXywSf2ShcGEAEJcAAHCwEAASQG8QcBElMPq2bT4KLY1zYjazQMOzZJ1AxqCgGTarjeAAA=";
[Theory]
[InlineData(false, "secret")]
[InlineData(true, "secret")]
[InlineData(false, "wrong")]
[InlineData(true, "wrong")]
public async Task SevenZipArchive_EncryptedHeader_PasswordErrors(bool async, string password)
{
using var input = new MemoryStream(Convert.FromBase64String(EncryptedHeader));
async Task ReadArchive()
{
if (async)
{
await using var archive = await SevenZipArchive.OpenAsyncArchive(
input,
new ReaderOptions { Password = password, LeaveStreamOpen = true },
CancellationToken.None
);
var count = 0;
await foreach (var entry in archive.EntriesAsync)
{
Assert.Equal("data.txt", entry.Key);
count++;
}
Assert.Equal(1, count);
}
else
{
using var archive = SevenZipArchive.OpenArchive(
input,
new ReaderOptions { Password = password, LeaveStreamOpen = true }
);
Assert.Equal("data.txt", archive.Entries.Single().Key);
}
}
if (password == "secret")
{
await ReadArchive();
}
else
{
var exception = await Assert.ThrowsAsync<CryptographicException>(ReadArchive);
Assert.IsType<InvalidFormatException>(exception.InnerException);
}
}
[Fact]
public async Task SevenZipArchive_WrongPassword_AsyncOpenPublicException()
{
await using var archive = await SevenZipArchive.OpenAsyncArchive(
Path.Combine(TEST_ARCHIVES_PATH, "7Zip.LZMA.Aes.7z"),
new ReaderOptions { Password = "wrong" },
CancellationToken.None
);
var exception = await Assert.ThrowsAsync<CryptographicException>(async () =>
{
await foreach (var entry in archive.EntriesAsync)
{
if (!entry.IsDirectory)
{
using var stream = await entry.OpenEntryStreamAsync(CancellationToken.None);
await stream.CopyToAsync(Stream.Null, 81920, CancellationToken.None);
}
}
});
Assert.IsType<DataErrorException>(exception.InnerException);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task SevenZipArchive_WrongPassword_PublicException(bool async)
{
using var archive = SevenZipArchive.OpenArchive(
Path.Combine(TEST_ARCHIVES_PATH, "7Zip.LZMA.Aes.7z"),
new ReaderOptions { Password = "wrong" }
);
var entry = archive.Entries.First(e => !e.IsDirectory);
var exception = await Assert.ThrowsAsync<CryptographicException>(async () =>
{
using var stream = entry.OpenEntryStream();
if (async)
{
await stream.CopyToAsync(Stream.Null, 81920, CancellationToken.None);
}
else
{
stream.CopyTo(Stream.Null);
}
});
Assert.IsType<DataErrorException>(exception.InnerException);
Assert.Contains("corrupt encrypted", exception.Message);
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.GZip;
using SharpCompress.Crypto;
using Xunit;
namespace SharpCompress.Test.Streams;
public class ChecksumReadRegressionTests
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task Zero_Length_Read_Does_Not_Validate(bool gzip)
{
byte[] payload = [1, 2, 3];
var crc = Crc32Stream.Compute(payload);
using var input = new MemoryStream(payload);
using var raw = new MemoryStream();
using Stream stream = gzip
? new GZipChecksumValidationStream(input, raw, "test", crc, 3)
: new ChecksumValidationStream(
input,
new ChecksumDescriptor(ChecksumKind.Crc32, crc, true),
"test"
);
Assert.Equal(0, stream.Read(Array.Empty<byte>(), 0, 0));
Assert.Equal(0, await stream.ReadAsync(Array.Empty<byte>(), 0, 0, CancellationToken.None));
#if !LEGACY_DOTNET
Assert.Equal(0, stream.Read(Span<byte>.Empty));
Assert.Equal(0, await stream.ReadAsync(Memory<byte>.Empty, CancellationToken.None));
#endif
using var output = new MemoryStream();
await stream.CopyToAsync(output, 81920, CancellationToken.None);
Assert.Equal(payload, output.ToArray());
}
}

View File

@@ -0,0 +1,48 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
namespace SharpCompress.Test.Streams;
public class DeflateReadAheadTests
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task Deflate_One_Byte_Output_Preserves_ReadAhead(bool async)
{
using var compressed = new MemoryStream();
using (
var writer = new System.IO.Compression.DeflateStream(
compressed,
System.IO.Compression.CompressionMode.Compress,
leaveOpen: true
)
)
{
writer.WriteByte(42);
}
byte[] trailer = [1, 2, 3, 4, 5, 6, 7, 8];
compressed.Write(trailer, 0, trailer.Length);
compressed.Position = 0;
using var decoder = new SharpCompress.Compressors.Deflate.DeflateStream(
compressed,
SharpCompress.Compressors.CompressionMode.Decompress
);
var buffer = new byte[1];
if (async)
{
Assert.Equal(1, await decoder.ReadAsync(buffer, 0, 1, CancellationToken.None));
Assert.Equal(0, await decoder.ReadAsync(buffer, 0, 1, CancellationToken.None));
}
else
{
Assert.Equal(1, decoder.Read(buffer, 0, 1));
Assert.Equal(0, decoder.Read(buffer, 0, 1));
}
Assert.Equal(42, buffer[0]);
using var remaining = decoder.InputBuffer;
Assert.Equal(trailer, remaining.ToArray());
}
}

View File

@@ -0,0 +1,30 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Compressors.LZMA;
using SharpCompress.Test.Mocks;
using Xunit;
namespace SharpCompress.Test.Streams;
public class LZipHeaderReadTests
{
[Theory]
[InlineData(12, 4096)]
[InlineData(29, 536870912)]
[InlineData(11, 0)]
[InlineData(30, 0)]
[InlineData(31, 0)]
[InlineData(44, 0)]
public async Task LZip_Header_Fragmentation_And_Dictionary_Validation(int encoded, int expected)
{
byte[] header = [0x4C, 0x5A, 0x49, 0x50, 1, (byte)encoded];
using var sync = new ShortReadStream(new MemoryStream(header));
using var asyncInput = new ShortReadStream(new MemoryStream(header));
Assert.Equal(expected, LZipStream.ValidateAndReadSize(sync));
Assert.Equal(
expected,
await LZipStream.ValidateAndReadSizeAsync(asyncInput, CancellationToken.None)
);
}
}

View File

@@ -4,14 +4,175 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Zip;
using SharpCompress.Test.Mocks;
using Xunit;
namespace SharpCompress.Test.Streams;
public class WinzipAesCryptoStreamTests
{
[Fact]
public void Dispose_Verifies_Unread_Ciphertext()
{
byte[] salt = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87];
var stream = CreateStream(
new byte[33],
"password",
salt,
fragmented: true,
corruption: "ciphertext"
);
Assert.Equal(1, stream.Read(new byte[1], 0, 1));
Assert.Throws<SharpCompress.Common.InvalidFormatException>(() => stream.Dispose());
stream.Dispose();
}
#if !LEGACY_DOTNET
[Fact]
public async Task DisposeAsync_Verifies_Unread_Ciphertext()
{
byte[] salt = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87];
var stream = CreateStream(
new byte[33],
"password",
salt,
fragmented: true,
corruption: "ciphertext"
);
Assert.Equal(1, await stream.ReadAsync(new byte[1], 0, 1, CancellationToken.None));
await Assert.ThrowsAsync<SharpCompress.Common.InvalidFormatException>(async () =>
await stream.DisposeAsync()
);
await stream.DisposeAsync();
}
#endif
[Theory]
[InlineData(false, 0)]
[InlineData(true, 0)]
[InlineData(false, 33)]
[InlineData(true, 33)]
public async Task Authentication_Allows_Fragmented_Reads_And_Empty_Payloads(
bool async,
int length
)
{
byte[] salt = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87];
byte[] payload = Enumerable.Range(0, length).Select(i => (byte)i).ToArray();
using var stream = CreateStream(payload, "password", salt, fragmented: true);
Assert.Equal(0, stream.Read(Array.Empty<byte>(), 0, 0));
using var output = new MemoryStream();
if (async)
{
await stream.CopyToAsync(output, 81920, CancellationToken.None);
}
else
{
stream.CopyTo(output);
}
Assert.Equal(payload, output.ToArray());
}
[Theory]
[InlineData(false, "ciphertext")]
[InlineData(true, "ciphertext")]
[InlineData(false, "missing-tag")]
[InlineData(true, "missing-tag")]
public async Task Authentication_Rejects_Tampering_And_Truncated_Tags(
bool async,
string corruption
)
{
byte[] salt = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87];
using var stream = CreateStream(
new byte[33],
"password",
salt,
fragmented: true,
corruption: corruption
);
using var output = new MemoryStream();
if (async)
{
var error = await Record.ExceptionAsync(async () =>
await stream.CopyToAsync(output, 81920, CancellationToken.None)
);
Assert.Equal(
corruption == "ciphertext"
? typeof(SharpCompress.Common.InvalidFormatException)
: typeof(SharpCompress.Common.IncompleteArchiveException),
error!.GetType()
);
Assert.Same(
error,
await Record.ExceptionAsync(async () =>
await stream.ReadAsync(new byte[1], 0, 1, CancellationToken.None)
)
);
}
else
{
var error = Record.Exception(() => stream.CopyTo(output));
Assert.Equal(
corruption == "ciphertext"
? typeof(SharpCompress.Common.InvalidFormatException)
: typeof(SharpCompress.Common.IncompleteArchiveException),
error!.GetType()
);
Assert.Same(error, Record.Exception(() => stream.Read(new byte[1], 0, 1)));
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task Read_Rejects_Modified_Authentication_Tag(bool async)
{
byte[] salt = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87];
var data = CreateEncryptionData("password", salt);
var cipher = EncryptCtr(new byte[32], data.KeyBytes);
using var input = new MemoryStream(cipher.Concat(new byte[10]).ToArray());
using var stream = new WinzipAesCryptoStream(input, data, cipher.Length);
if (async)
{
await Assert.ThrowsAsync<SharpCompress.Common.InvalidFormatException>(async () =>
await stream.ReadAsync(new byte[32], 0, 32)
);
}
else
{
Assert.Throws<SharpCompress.Common.InvalidFormatException>(() =>
stream.Read(new byte[32], 0, 32)
);
}
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task Read_Rejects_Truncated_Ciphertext(bool async)
{
byte[] salt = [0x10, 0x21, 0x32, 0x43, 0x54, 0x65, 0x76, 0x87];
var data = CreateEncryptionData("password", salt);
using var input = new MemoryStream();
using var stream = new WinzipAesCryptoStream(input, data, 32);
if (async)
{
await Assert.ThrowsAsync<SharpCompress.Common.IncompleteArchiveException>(async () =>
await stream.ReadAsync(new byte[32], 0, 32)
);
}
else
{
Assert.Throws<SharpCompress.Common.IncompleteArchiveException>(() =>
stream.Read(new byte[32], 0, 32)
);
}
}
[Fact]
public void Read_Decrypts_Data_For_Aligned_Buffer_Size()
{
@@ -104,17 +265,33 @@ public class WinzipAesCryptoStreamTests
private static WinzipAesCryptoStream CreateStream(
byte[] plainText,
string password,
byte[] salt
byte[] salt,
bool fragmented = false,
string? corruption = null
)
{
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
);
#pragma warning disable CA5350 // WinZip AES mandates HMAC-SHA1-80.
using var authentication = new HMACSHA1(encryptionData.IvBytes);
#pragma warning restore CA5350
byte[] archiveBytes = cipherText
.Concat(authentication.ComputeHash(cipherText).Take(10))
.ToArray();
if (corruption == "ciphertext")
{
archiveBytes[0] ^= 1;
}
else if (corruption == "missing-tag")
{
archiveBytes = archiveBytes.Take(archiveBytes.Length - 1).ToArray();
}
Stream input = new MemoryStream(archiveBytes, writable: false);
if (fragmented)
{
input = new ShortReadStream(input);
}
return new WinzipAesCryptoStream(input, encryptionData, cipherText.Length);
}
[SuppressMessage(

View File

@@ -0,0 +1,78 @@
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives.Zip;
using SharpCompress.Readers;
using SharpCompress.Readers.Zip;
using SharpCompress.Test.Mocks;
using Xunit;
namespace SharpCompress.Test.Zip;
public class ZipAesShortReadTests : TestBase
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task Zip_Aes_Archive_Reads_Fragmented_Metadata(bool async)
{
using var input = new ShortReadStream(
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip"))
);
using var output = new MemoryStream();
var options = new ReaderOptions { Password = "test", LeaveStreamOpen = true };
if (async)
{
await using var archive = await ZipArchive.OpenAsyncArchive(
input,
options,
CancellationToken.None
);
await foreach (var entry in archive.EntriesAsync)
{
if (entry.IsDirectory)
{
continue;
}
using var entryStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
await entryStream.CopyToAsync(output, 81920, CancellationToken.None);
}
}
else
{
using var archive = ZipArchive.OpenArchive(input, options);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
using var entryStream = entry.OpenEntryStream();
entryStream.CopyTo(output);
}
}
Assert.True(output.Length > 0);
Assert.False(input.IsDisposed);
}
[Fact]
public void Zip_Aes_Reader_Reads_Fragmented_Metadata()
{
using var input = new ShortReadStream(
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip")),
seekable: false
);
using var reader = ZipReader.OpenReader(
input,
new ReaderOptions { Password = "test", LeaveStreamOpen = true }
);
using var output = new MemoryStream();
while (reader.MoveToNextEntry())
{
if (reader.Entry.IsDirectory)
{
continue;
}
using var entryStream = reader.OpenEntryStream();
entryStream.CopyTo(output);
}
Assert.True(output.Length > 0);
}
}

View File

@@ -159,6 +159,72 @@ public class ZipArchiveTests : ArchiveTests
Assert.Equal(new[] { zip, z01 }, ArchiveFactory.GetFileParts(zip));
}
[Theory]
[InlineData("MULTI.ZIP", "MULTI.Z01", "Infozip.nocomp.multi")]
[InlineData("MULTI.ZiP", "Multi.z01", "Infozip.nocomp.multi")]
[InlineData("MULTI.ZIPX", "MULTI.ZX01", "WinZip26.nocomp.multi")]
[InlineData("MULTI.ZiPx", "Multi.zX01", "WinZip26.nocomp.multi")]
public void Zip_GetParts_MixedCaseCompanions(
string archiveName,
string partName,
string fixture
)
{
var zip = Path.Combine(SCRATCH2_FILES_PATH, archiveName);
var part = Path.Combine(SCRATCH2_FILES_PATH, partName);
var isZipX = archiveName.EndsWith("zipx", StringComparison.OrdinalIgnoreCase);
File.Copy(Path.Combine(TEST_ARCHIVES_PATH, fixture + (isZipX ? ".zipx" : ".zip")), zip);
File.Copy(Path.Combine(TEST_ARCHIVES_PATH, fixture + (isZipX ? ".zx01" : ".z01")), part);
var parts = ArchiveFactory.GetFileParts(zip).ToArray();
Assert.Equal(2, parts.Length);
Assert.Equal(zip, parts[0]);
Assert.True(string.Equals(part, parts[1], StringComparison.OrdinalIgnoreCase));
using var archive = ZipArchive.OpenArchive(zip);
Assert.NotEmpty(archive.Entries);
}
[Fact]
public void Zip_GetParts_RejectsAmbiguousCaseInsensitiveMatches()
{
var zip = Path.Combine(SCRATCH2_FILES_PATH, "MULTI.ZIP");
File.WriteAllBytes(zip, []);
File.WriteAllBytes(Path.Combine(SCRATCH2_FILES_PATH, "Multi.Z01"), []);
var other = Path.Combine(SCRATCH2_FILES_PATH, "multi.Z01");
if (File.Exists(other))
{
// Case-insensitive filesystems cannot represent an ambiguous pair.
return;
}
File.WriteAllBytes(other, []);
Assert.Throws<InvalidFormatException>(() => ArchiveFactory.GetFileParts(zip).ToArray());
}
[Fact]
public void Zip_GetParts_PrefersExactMatch()
{
var zip = Path.Combine(SCRATCH2_FILES_PATH, "MULTI.ZIP");
var part = Path.Combine(SCRATCH2_FILES_PATH, "MULTI.z01");
File.WriteAllBytes(zip, []);
File.WriteAllBytes(part, []);
var other = Path.Combine(SCRATCH2_FILES_PATH, "Multi.Z01");
if (!File.Exists(other))
{
File.WriteAllBytes(other, []);
}
Assert.Equal(new[] { zip, part }, ArchiveFactory.GetFileParts(zip));
}
[Fact]
public void Zip_GetParts_AllowsMissingCompanion()
{
var zip = Path.Combine(SCRATCH2_FILES_PATH, "MULTI.ZIP");
File.WriteAllBytes(zip, []);
Assert.Equal(new[] { zip }, ArchiveFactory.GetFileParts(zip));
}
[Fact]
public void Zip_GetPartsSplit() =>
//uses first part to search for all parts and compares against this array