mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-11-15 17:02:03 +00:00
Merge remote-tracking branch 'origin/release' into fix/1441-sevenzip-libarchive-attributes
This commit is contained in:
20
docs/API.md
20
docs/API.md
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}.");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
58
src/SharpCompress/Compressors/LZMA/EncryptedDecoderStream.cs
Normal file
58
src/SharpCompress/Compressors/LZMA/EncryptedDecoderStream.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
21
tests/SharpCompress.Test/Ace/AceHeaderReadTests.cs
Normal file
21
tests/SharpCompress.Test/Ace/AceHeaderReadTests.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
26
tests/SharpCompress.Test/Arc/ArcHeaderReadTests.cs
Normal file
26
tests/SharpCompress.Test/Arc/ArcHeaderReadTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
53
tests/SharpCompress.Test/Arj/ArjHeaderReadTests.cs
Normal file
53
tests/SharpCompress.Test/Arj/ArjHeaderReadTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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)]
|
||||
|
||||
45
tests/SharpCompress.Test/GZip/GZipTrailerReadTests.cs
Normal file
45
tests/SharpCompress.Test/GZip/GZipTrailerReadTests.cs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
tests/SharpCompress.Test/Mocks/ShortReadStream.cs
Normal file
28
tests/SharpCompress.Test/Mocks/ShortReadStream.cs
Normal 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
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
120
tests/SharpCompress.Test/Rar/RarCryptoWrapperTests.cs
Normal file
120
tests/SharpCompress.Test/Rar/RarCryptoWrapperTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 =
|
||||
[
|
||||
|
||||
45
tests/SharpCompress.Test/Rar/RarVolumeCasingTests.cs
Normal file
45
tests/SharpCompress.Test/Rar/RarVolumeCasingTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
tests/SharpCompress.Test/SevenZip/SevenZipAesKdfTests.cs
Normal file
35
tests/SharpCompress.Test/SevenZip/SevenZipAesKdfTests.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
48
tests/SharpCompress.Test/Streams/DeflateReadAheadTests.cs
Normal file
48
tests/SharpCompress.Test/Streams/DeflateReadAheadTests.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
30
tests/SharpCompress.Test/Streams/LZipHeaderReadTests.cs
Normal file
30
tests/SharpCompress.Test/Streams/LZipHeaderReadTests.cs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
78
tests/SharpCompress.Test/Zip/ZipAesShortReadTests.cs
Normal file
78
tests/SharpCompress.Test/Zip/ZipAesShortReadTests.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user