mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-11-15 17:02:03 +00:00
21
docs/API.md
21
docs/API.md
@@ -101,6 +101,27 @@ The stream overloads of `DetectArchive` and `InspectArchive` preserve the suppli
|
||||
| `ZipDataDescriptorEntryCount` | `Zip.HasEntriesWithDeferredSizes` |
|
||||
| `SolidStreamCount` | `SolidStreamCount` |
|
||||
|
||||
### RAR5 Archive Comments
|
||||
|
||||
`SharpCompress.Common.Rar.RarVolume.Comment` exposes UTF-8 archive comments, including encrypted comments when `ReaderOptions.Password` is supplied. Comments are loaded while entries are enumerated; the property can be `null` before enumeration or when no comment is present.
|
||||
|
||||
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.
|
||||
|
||||
```csharp
|
||||
using var archive = RarArchive.OpenArchive(
|
||||
"encrypted.rar",
|
||||
ReaderOptions.ForFilePath.WithPassword("password")
|
||||
);
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
Console.WriteLine(entry.Key);
|
||||
}
|
||||
foreach (var volume in archive.Volumes.OfType<SharpCompress.Common.Rar.RarVolume>())
|
||||
{
|
||||
Console.WriteLine(volume.Comment);
|
||||
}
|
||||
```
|
||||
|
||||
### Creating Archives
|
||||
|
||||
```csharp
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.302",
|
||||
"version": "10.0.401",
|
||||
"rollForward": "disable"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ internal sealed class FileInfoRarFilePart : SeekableFilePart
|
||||
FileHeader fh,
|
||||
FileInfo fi
|
||||
)
|
||||
: base(mh, fh, volume.Index, volume.Stream, password) => FileInfo = fi;
|
||||
: base(mh, fh, volume.Index, volume.Stream, password, volume.KeyCache) => FileInfo = fi;
|
||||
|
||||
internal FileInfo FileInfo { get; }
|
||||
|
||||
|
||||
@@ -8,18 +8,21 @@ internal class SeekableFilePart : RarFilePart
|
||||
{
|
||||
private readonly Stream _stream;
|
||||
private readonly string? _password;
|
||||
private readonly Rar5KeyCache _keyCache;
|
||||
|
||||
internal SeekableFilePart(
|
||||
MarkHeader mh,
|
||||
FileHeader fh,
|
||||
int index,
|
||||
Stream stream,
|
||||
string? password
|
||||
string? password,
|
||||
Rar5KeyCache keyCache
|
||||
)
|
||||
: base(mh, fh, index)
|
||||
{
|
||||
_stream = stream;
|
||||
_password = password;
|
||||
_keyCache = keyCache;
|
||||
}
|
||||
|
||||
internal override Stream GetCompressedStream()
|
||||
@@ -34,7 +37,7 @@ internal class SeekableFilePart : RarFilePart
|
||||
|
||||
if (FileHeader.Rar5CryptoInfo != null)
|
||||
{
|
||||
var cryptKey = new CryptKey5(_password!, FileHeader.Rar5CryptoInfo);
|
||||
var cryptKey = new CryptKey5(_password!, FileHeader.Rar5CryptoInfo, _keyCache);
|
||||
return new RarCryptoWrapper(_stream, FileHeader.Rar5CryptoInfo.Salt, cryptKey);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,5 +18,12 @@ internal class StreamRarArchiveVolume : RarVolume
|
||||
GetVolumeFilePartsAsync();
|
||||
|
||||
internal override RarFilePart CreateFilePart(MarkHeader markHeader, FileHeader fileHeader) =>
|
||||
new SeekableFilePart(markHeader, fileHeader, Index, Stream, ReaderOptions.Password);
|
||||
new SeekableFilePart(
|
||||
markHeader,
|
||||
fileHeader,
|
||||
Index,
|
||||
Stream,
|
||||
ReaderOptions.Password,
|
||||
KeyCache
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ internal static class ZipArchiveVolumeFactory
|
||||
part1.DirectoryName!,
|
||||
String.Concat(
|
||||
m.Groups[1].Value,
|
||||
Regex.Replace(m.Groups[2].Value, @"[^xz]", ""),
|
||||
Regex.Replace(m.Groups[2].Value.ToLowerInvariant(), @"[^xz]", ""),
|
||||
index.ToString(Constants.DefaultCultureInfo).PadLeft(2, '0')
|
||||
)
|
||||
)
|
||||
|
||||
@@ -83,7 +83,7 @@ internal sealed partial class GZipFilePart
|
||||
var lengthField = new byte[2];
|
||||
_ = await _stream.ReadAsync(lengthField, 0, 2, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var extraLength = (short)(lengthField[0] + (lengthField[1] * 256));
|
||||
var extraLength = lengthField[0] + (lengthField[1] * 256);
|
||||
var extra = new byte[extraLength];
|
||||
|
||||
if (!await _stream.ReadFullyAsync(extra, cancellationToken).ConfigureAwait(false))
|
||||
|
||||
@@ -87,7 +87,7 @@ internal sealed partial class GZipFilePart : FilePart
|
||||
// read and discard extra field
|
||||
n = _stream.Read(header.Slice(0, 2)); // 2-byte length field
|
||||
|
||||
var extraLength = (short)(header[0] + (header[1] * 256));
|
||||
var extraLength = header[0] + (header[1] * 256);
|
||||
var extra = new byte[extraLength];
|
||||
|
||||
if (!_stream.ReadFully(extra))
|
||||
|
||||
@@ -9,32 +9,32 @@ namespace SharpCompress.Common.Rar;
|
||||
internal class CryptKey5 : ICryptKey
|
||||
{
|
||||
const int AES_256 = 256;
|
||||
const int DERIVED_KEY_LENGTH = 0x10;
|
||||
const int SHA256_DIGEST_SIZE = 32;
|
||||
|
||||
private string _password;
|
||||
private Rar5CryptoInfo _cryptoInfo;
|
||||
private readonly string _password;
|
||||
private readonly Rar5CryptoInfo _cryptoInfo;
|
||||
private readonly Rar5KeyCache _keyCache;
|
||||
private byte[]? _aesKey;
|
||||
private byte[] _derivedSalt = [];
|
||||
private int _derivedLg2Count;
|
||||
private byte[] _pswCheck = [];
|
||||
private byte[] _hashKey = [];
|
||||
|
||||
public CryptKey5(string? password, Rar5CryptoInfo rar5CryptoInfo)
|
||||
public CryptKey5(string? password, Rar5CryptoInfo rar5CryptoInfo, Rar5KeyCache? keyCache = null)
|
||||
{
|
||||
_password = password ?? "";
|
||||
_cryptoInfo = rar5CryptoInfo;
|
||||
_keyCache = keyCache ?? new Rar5KeyCache();
|
||||
}
|
||||
|
||||
public byte[] PswCheck => _pswCheck;
|
||||
|
||||
public byte[] HashKey => _hashKey;
|
||||
|
||||
private static List<byte[]> GenerateRarPBKDF2Key(
|
||||
string password,
|
||||
byte[] salt,
|
||||
int iterations,
|
||||
int keyLength
|
||||
)
|
||||
internal static List<byte[]> GenerateRarPBKDF2Key(string password, byte[] salt, int iterations)
|
||||
{
|
||||
var passwordBytes = Encoding.UTF8.GetBytes(password);
|
||||
salt = salt.Concat(new byte[] { 0, 0, 0, 1 }).ToArray();
|
||||
#if LEGACY_DOTNET
|
||||
using var hmac = new HMACSHA256(passwordBytes);
|
||||
var block = hmac.ComputeHash(salt);
|
||||
@@ -69,23 +69,24 @@ internal class CryptKey5 : ICryptKey
|
||||
|
||||
public ICryptoTransform Transformer(byte[] salt)
|
||||
{
|
||||
var iterations = (1 << _cryptoInfo.LG2Count); // Adjust the number of iterations as needed
|
||||
|
||||
var salt_rar5 = salt.Concat(new byte[] { 0, 0, 0, 1 });
|
||||
var derivedKey = GenerateRarPBKDF2Key(
|
||||
_password,
|
||||
salt_rar5.ToArray(),
|
||||
iterations,
|
||||
DERIVED_KEY_LENGTH
|
||||
);
|
||||
|
||||
_hashKey = derivedKey[1];
|
||||
|
||||
_pswCheck = new byte[EncryptionConstV5.SIZE_PSWCHECK];
|
||||
|
||||
for (var i = 0; i < SHA256_DIGEST_SIZE; i++)
|
||||
// Header IVs change for each block, but the expensive KDF inputs do not.
|
||||
// Snapshot the salt so in-place changes cannot reuse an unrelated key.
|
||||
if (
|
||||
_aesKey is null
|
||||
|| _derivedLg2Count != _cryptoInfo.LG2Count
|
||||
|| !_derivedSalt.SequenceEqual(salt)
|
||||
)
|
||||
{
|
||||
_pswCheck[i % EncryptionConstV5.SIZE_PSWCHECK] ^= derivedKey[2][i];
|
||||
var derivedKey = _keyCache.GetKeys(_password, salt, _cryptoInfo.LG2Count);
|
||||
_hashKey = derivedKey[1];
|
||||
_pswCheck = new byte[EncryptionConstV5.SIZE_PSWCHECK];
|
||||
for (var i = 0; i < SHA256_DIGEST_SIZE; i++)
|
||||
{
|
||||
_pswCheck[i % EncryptionConstV5.SIZE_PSWCHECK] ^= derivedKey[2][i];
|
||||
}
|
||||
_derivedSalt = (byte[])salt.Clone();
|
||||
_derivedLg2Count = _cryptoInfo.LG2Count;
|
||||
_aesKey = derivedKey[0];
|
||||
}
|
||||
|
||||
if (_cryptoInfo.UsePswCheck && !_cryptoInfo.PswCheck.SequenceEqual(_pswCheck))
|
||||
@@ -93,11 +94,11 @@ internal class CryptKey5 : ICryptKey
|
||||
throw new CryptographicException("The password did not match.");
|
||||
}
|
||||
|
||||
var aes = Aes.Create();
|
||||
using var aes = Aes.Create();
|
||||
aes.KeySize = AES_256;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.None;
|
||||
aes.Key = derivedKey[0];
|
||||
aes.Key = _aesKey;
|
||||
aes.IV = _cryptoInfo.InitV;
|
||||
return aes.CreateDecryptor();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public partial class RarHeaderFactory
|
||||
{
|
||||
public async IAsyncEnumerable<IRarHeader> ReadHeadersAsync(Stream stream)
|
||||
{
|
||||
ResetHeaderScanState();
|
||||
var markHeader = await MarkHeader
|
||||
.ReadAsync(
|
||||
stream,
|
||||
@@ -67,10 +68,8 @@ public partial class RarHeaderFactory
|
||||
await _cryptInfo
|
||||
.ReadInitVAsync(new AsyncMarkingBinaryReader(stream))
|
||||
.ConfigureAwait(false);
|
||||
var _headerKey = new CryptKey5(Options.Password!, _cryptInfo);
|
||||
|
||||
reader = await AsyncRarCryptoBinaryReader
|
||||
.Create(stream, _headerKey, _cryptInfo.Salt)
|
||||
.Create(stream, GetHeaderKey(), _cryptInfo.Salt)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
@@ -136,9 +135,9 @@ public partial class RarHeaderFactory
|
||||
var fh = await FileHeader
|
||||
.CreateAsync(header, reader, HeaderType.Service, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (fh.FileName == "CMT")
|
||||
if (fh.FileName == "CMT" && fh.IsStored)
|
||||
{
|
||||
fh.PackedStream = new ReadOnlySubStream(reader.BaseStream, fh.CompressedSize);
|
||||
fh.PackedStream = CreatePackedStream(fh, reader.BaseStream);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -187,7 +186,8 @@ public partial class RarHeaderFactory
|
||||
fh.R4Salt is null
|
||||
? new CryptKey5(
|
||||
Options.Password,
|
||||
fh.Rar5CryptoInfo.NotNull()
|
||||
fh.Rar5CryptoInfo.NotNull(),
|
||||
KeyCache
|
||||
)
|
||||
: new CryptKey3(Options.Password)
|
||||
);
|
||||
@@ -215,6 +215,7 @@ public partial class RarHeaderFactory
|
||||
.ConfigureAwait(false);
|
||||
IsEncrypted = true;
|
||||
_cryptInfo = cryptoHeader.CryptInfo;
|
||||
_headerKey = null;
|
||||
|
||||
return cryptoHeader;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,21 @@ public partial class RarHeaderFactory
|
||||
private bool _isRar5;
|
||||
|
||||
private Rar5CryptoInfo? _cryptInfo;
|
||||
private CryptKey5? _headerKey;
|
||||
private string? _headerPassword;
|
||||
internal Rar5KeyCache KeyCache { get; } = new();
|
||||
|
||||
private CryptKey5 GetHeaderKey()
|
||||
{
|
||||
// ReaderOptions is mutable; changing its password must not keep using
|
||||
// a key derived from the previous password.
|
||||
if (_headerKey is null || _headerPassword != Options.Password)
|
||||
{
|
||||
_headerPassword = Options.Password;
|
||||
_headerKey = new CryptKey5(_headerPassword, _cryptInfo.NotNull(), KeyCache);
|
||||
}
|
||||
return _headerKey;
|
||||
}
|
||||
|
||||
public RarHeaderFactory(StreamingMode mode, ReaderOptions options)
|
||||
{
|
||||
@@ -24,8 +39,20 @@ public partial class RarHeaderFactory
|
||||
public StreamingMode StreamingMode { get; }
|
||||
public bool IsEncrypted { get; private set; }
|
||||
|
||||
private void ResetHeaderScanState()
|
||||
{
|
||||
// Metadata probes rewind the stream. Each new scan must rediscover encryption
|
||||
// from the plaintext archive headers, rather than reuse the previous scan's state.
|
||||
IsEncrypted = false;
|
||||
_cryptInfo = null;
|
||||
_headerKey = null;
|
||||
_headerPassword = null;
|
||||
// Keep the password/salt/iteration-keyed KDF cache across scans.
|
||||
}
|
||||
|
||||
public IEnumerable<IRarHeader> ReadHeaders(Stream stream)
|
||||
{
|
||||
ResetHeaderScanState();
|
||||
var markHeader = MarkHeader.Read(stream, Options.LeaveStreamOpen, Options.LookForHeader);
|
||||
_isRar5 = markHeader.IsRar5;
|
||||
yield return markHeader;
|
||||
@@ -62,9 +89,7 @@ public partial class RarHeaderFactory
|
||||
if (_isRar5 && _cryptInfo != null)
|
||||
{
|
||||
_cryptInfo.ReadInitV(new MarkingBinaryReader(stream));
|
||||
var _headerKey = new CryptKey5(Options.Password!, _cryptInfo);
|
||||
|
||||
reader = RarCryptoBinaryReader.Create(stream, _headerKey, _cryptInfo.Salt);
|
||||
reader = RarCryptoBinaryReader.Create(stream, GetHeaderKey(), _cryptInfo.Salt);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -120,9 +145,10 @@ public partial class RarHeaderFactory
|
||||
case HeaderCodeV.RAR5_SERVICE_HEADER:
|
||||
{
|
||||
var fh = FileHeader.Create(header, reader, HeaderType.Service);
|
||||
if (fh.FileName == "CMT")
|
||||
// Only stored comments are supported; skip other methods without decrypting.
|
||||
if (fh.FileName == "CMT" && fh.IsStored)
|
||||
{
|
||||
fh.PackedStream = new ReadOnlySubStream(reader.BaseStream, fh.CompressedSize);
|
||||
fh.PackedStream = CreatePackedStream(fh, reader.BaseStream);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -167,7 +193,8 @@ public partial class RarHeaderFactory
|
||||
fh.R4Salt is null
|
||||
? new CryptKey5(
|
||||
Options.Password,
|
||||
fh.Rar5CryptoInfo.NotNull()
|
||||
fh.Rar5CryptoInfo.NotNull(),
|
||||
KeyCache
|
||||
)
|
||||
: new CryptKey3(Options.Password)
|
||||
);
|
||||
@@ -191,6 +218,7 @@ public partial class RarHeaderFactory
|
||||
var cryptoHeader = ArchiveCryptHeader.Create(header, reader);
|
||||
IsEncrypted = true;
|
||||
_cryptInfo = cryptoHeader.CryptInfo;
|
||||
_headerKey = null;
|
||||
|
||||
return cryptoHeader;
|
||||
}
|
||||
@@ -223,4 +251,20 @@ public partial class RarHeaderFactory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Stream CreatePackedStream(FileHeader header, Stream stream)
|
||||
{
|
||||
var packedStream = new ReadOnlySubStream(stream, header.CompressedSize);
|
||||
// Service data has its own encryption metadata, independent of header encryption.
|
||||
if (header.Rar5CryptoInfo is not null)
|
||||
{
|
||||
return new RarCryptoWrapper(
|
||||
packedStream,
|
||||
header.Rar5CryptoInfo.Salt,
|
||||
new CryptKey5(Options.Password, header.Rar5CryptoInfo, KeyCache)
|
||||
);
|
||||
}
|
||||
|
||||
return packedStream;
|
||||
}
|
||||
}
|
||||
|
||||
40
src/SharpCompress/Common/Rar/Rar5KeyCache.cs
Normal file
40
src/SharpCompress/Common/Rar/Rar5KeyCache.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace SharpCompress.Common.Rar;
|
||||
|
||||
// Cache only KDF output, never entry-specific IVs, checks, or AES transforms.
|
||||
internal sealed class Rar5KeyCache
|
||||
{
|
||||
private const int Capacity = 4;
|
||||
private readonly List<(string Password, byte[] Salt, int Lg2Count, List<byte[]> Keys)> entries =
|
||||
new();
|
||||
|
||||
internal List<byte[]> GetKeys(string password, byte[] salt, int lg2Count)
|
||||
{
|
||||
lock (entries)
|
||||
{
|
||||
foreach (var (cachedPassword, cachedSalt, cachedCount, cachedKeys) in entries)
|
||||
{
|
||||
if (
|
||||
cachedPassword == password
|
||||
&& cachedCount == lg2Count
|
||||
&& cachedSalt.SequenceEqual(salt)
|
||||
)
|
||||
{
|
||||
return cachedKeys;
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot mutable metadata so later changes cannot corrupt cache identity.
|
||||
var snapshot = (byte[])salt.Clone();
|
||||
var keys = CryptKey5.GenerateRarPBKDF2Key(password, snapshot, 1 << lg2Count);
|
||||
if (entries.Count == Capacity)
|
||||
{
|
||||
entries.RemoveAt(0);
|
||||
}
|
||||
entries.Add((password, snapshot, lg2Count, keys));
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -17,7 +18,10 @@ namespace SharpCompress.Common.Rar;
|
||||
/// </summary>
|
||||
public abstract class RarVolume : Volume
|
||||
{
|
||||
private const int MaxCommentSize = 16 * 1024 * 1024;
|
||||
|
||||
private readonly RarHeaderFactory _headerFactory;
|
||||
internal Rar5KeyCache KeyCache => _headerFactory.KeyCache;
|
||||
private int _maxCompressionAlgorithm;
|
||||
|
||||
internal RarVolume(StreamingMode mode, Stream stream, ReaderOptions options, int index)
|
||||
@@ -66,11 +70,25 @@ public abstract class RarVolume : Volume
|
||||
case HeaderType.Service:
|
||||
{
|
||||
var fh = (FileHeader)header;
|
||||
if (fh.FileName == "CMT")
|
||||
if (fh.FileName == "CMT" && fh.IsStored)
|
||||
{
|
||||
var buffer = new byte[fh.CompressedSize];
|
||||
fh.PackedStream.NotNull().ReadFully(buffer);
|
||||
Comment = Encoding.UTF8.GetString(buffer, 0, buffer.Length - 1);
|
||||
// Read the logical service size, not the potentially padded packed size.
|
||||
var commentSize = GetCommentSize(fh);
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(commentSize);
|
||||
try
|
||||
{
|
||||
using var packedStream = fh.PackedStream.NotNull();
|
||||
if (!packedStream.ReadFully(buffer.AsSpan(0, commentSize)))
|
||||
{
|
||||
// Never decode unread bytes from a pooled buffer.
|
||||
throw new EndOfStreamException();
|
||||
}
|
||||
Comment = DecodeComment(buffer, commentSize);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -115,14 +133,28 @@ public abstract class RarVolume : Volume
|
||||
case HeaderType.Service:
|
||||
{
|
||||
var fh = (FileHeader)header;
|
||||
if (fh.FileName == "CMT")
|
||||
if (fh.FileName == "CMT" && fh.IsStored)
|
||||
{
|
||||
var buffer = new byte[fh.CompressedSize];
|
||||
await fh
|
||||
.PackedStream.NotNull()
|
||||
.ReadFullyAsync(buffer, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
Comment = Encoding.UTF8.GetString(buffer, 0, buffer.Length - 1);
|
||||
var commentSize = GetCommentSize(fh);
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(commentSize);
|
||||
try
|
||||
{
|
||||
using var packedStream = fh.PackedStream.NotNull();
|
||||
if (
|
||||
!await packedStream
|
||||
.ReadFullyAsync(buffer, 0, commentSize, cancellationToken)
|
||||
.ConfigureAwait(false)
|
||||
)
|
||||
{
|
||||
// Never decode unread bytes from a pooled buffer.
|
||||
throw new EndOfStreamException();
|
||||
}
|
||||
Comment = DecodeComment(buffer, commentSize);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -130,6 +162,28 @@ public abstract class RarVolume : Volume
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetCommentSize(FileHeader header)
|
||||
{
|
||||
// Match UnRAR's limit for in-memory service data before renting a buffer.
|
||||
// Unknown unpacked sizes are represented by long.MaxValue and rejected too.
|
||||
if (header.UncompressedSize < 0 || header.UncompressedSize > MaxCommentSize)
|
||||
{
|
||||
throw new InvalidFormatException("RAR archive comment exceeds the 16 MiB size limit.");
|
||||
}
|
||||
|
||||
return (int)header.UncompressedSize;
|
||||
}
|
||||
|
||||
private static string DecodeComment(byte[] buffer, int commentSize)
|
||||
{
|
||||
// Like UnRAR, stop at the first NUL. Encrypted comments can include
|
||||
// padding in their unpacked size as well as their packed size.
|
||||
// Limit decoding to the payload, since rented buffers can be larger.
|
||||
var terminator = Array.IndexOf(buffer, (byte)0, 0, commentSize);
|
||||
var length = terminator < 0 ? commentSize : terminator;
|
||||
return Encoding.UTF8.GetString(buffer, 0, length);
|
||||
}
|
||||
|
||||
private void EnsureArchiveHeaderLoaded()
|
||||
{
|
||||
if (ArchiveHeader is null)
|
||||
|
||||
@@ -104,6 +104,13 @@ internal sealed partial class ArchiveReader
|
||||
throw new ArchiveOperationException("nextHeaderOffset is invalid");
|
||||
}
|
||||
|
||||
// Validate the physical header range before allocating attacker-controlled sizes.
|
||||
// Subtract the validated offset instead of adding offset and size to avoid overflow.
|
||||
if (nextHeaderSize > _streamEnding - db._startPositionAfterHeader - nextHeaderOffset)
|
||||
{
|
||||
throw new IncompleteArchiveException("Next header extends beyond the end of stream.");
|
||||
}
|
||||
|
||||
_stream.Seek(nextHeaderOffset, SeekOrigin.Current);
|
||||
|
||||
var header = new byte[nextHeaderSize];
|
||||
|
||||
@@ -1043,6 +1043,13 @@ internal partial class ArchiveReader
|
||||
throw new ArchiveOperationException("nextHeaderOffset is invalid");
|
||||
}
|
||||
|
||||
// Validate the physical header range before allocating attacker-controlled sizes.
|
||||
// Subtract the validated offset instead of adding offset and size to avoid overflow.
|
||||
if (nextHeaderSize > _streamEnding - db._startPositionAfterHeader - nextHeaderOffset)
|
||||
{
|
||||
throw new IncompleteArchiveException("Next header extends beyond the end of stream.");
|
||||
}
|
||||
|
||||
_stream.Seek(nextHeaderOffset, SeekOrigin.Current);
|
||||
|
||||
var header = new byte[nextHeaderSize];
|
||||
|
||||
@@ -660,7 +660,7 @@ internal partial class ZlibBaseStream : Stream, IStreamStack
|
||||
n = _stream.Read(header.Slice(0, 2)); // 2-byte length field
|
||||
totalBytesRead += n;
|
||||
|
||||
var extraLength = (short)(header[0] + header[1] * 256);
|
||||
var extraLength = header[0] + (header[1] * 256);
|
||||
var extra = new byte[extraLength];
|
||||
n = _stream.Read(extra, 0, extra.Length);
|
||||
if (n != extraLength)
|
||||
@@ -720,7 +720,7 @@ internal partial class ZlibBaseStream : Stream, IStreamStack
|
||||
n = await _stream.ReadAsync(header, 0, 2, cancellationToken).ConfigureAwait(false); // 2-byte length field
|
||||
totalBytesRead += n;
|
||||
|
||||
var extraLength = (short)(header[0] + header[1] * 256);
|
||||
var extraLength = header[0] + (header[1] * 256);
|
||||
var extra = new byte[extraLength];
|
||||
n = await _stream
|
||||
.ReadAsync(extra, 0, extra.Length, cancellationToken)
|
||||
|
||||
@@ -339,9 +339,9 @@
|
||||
"net10.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg=="
|
||||
"requested": "[10.0.12, )",
|
||||
"resolved": "10.0.12",
|
||||
"contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
@@ -471,9 +471,9 @@
|
||||
"net8.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.29, )",
|
||||
"resolved": "8.0.29",
|
||||
"contentHash": "HSBTfrkIZijz8z3ybLRKB7E8rHk4QQufFwpHa9fc5CMIgRhRzdn4mBGmlyXZqaueiMPtuJcnjresGvSTfaW8Mg=="
|
||||
"requested": "[8.0.31, )",
|
||||
"resolved": "8.0.31",
|
||||
"contentHash": "OJdAGATV2QoVWL11zlDLEluA06q8rK+cgZR47ohrO8V+Qj5z9dlvN7bG9/0e5t0IOe+yD8tREC0bpsEgDGrzIQ=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
"net10.0": {
|
||||
"Microsoft.DotNet.ILCompiler": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "tnG8ntt/Bk6odvHREnGLMo3PEiihy5iSlIFVp0JbIo00GKtNRt2k73eKZbPqR5yaJNIa3z8R86YLwbxfqpb17g=="
|
||||
"requested": "[10.0.12, )",
|
||||
"resolved": "10.0.12",
|
||||
"contentHash": "AawF393Q+VkdrnrnI1gu612zh5iqpa1AGSvnKCQ3IkMgSKJXIQbO1sXYRgEzOU4f0cPZ6MaCCF29xZSGWlmbuQ=="
|
||||
},
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg=="
|
||||
"requested": "[10.0.12, )",
|
||||
"resolved": "10.0.12",
|
||||
"contentHash": "xi+BDjFpW+Sb+MHFHaH6Y/gV9I8BluFwRXc1QyCdoZbIK26eNiBeFuMTe/FMwc33G1wdHCyDg7CVTmb8OdQrMQ=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
@@ -82,17 +82,17 @@
|
||||
"net10.0/linux-x64": {
|
||||
"Microsoft.DotNet.ILCompiler": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "tnG8ntt/Bk6odvHREnGLMo3PEiihy5iSlIFVp0JbIo00GKtNRt2k73eKZbPqR5yaJNIa3z8R86YLwbxfqpb17g==",
|
||||
"requested": "[10.0.12, )",
|
||||
"resolved": "10.0.12",
|
||||
"contentHash": "AawF393Q+VkdrnrnI1gu612zh5iqpa1AGSvnKCQ3IkMgSKJXIQbO1sXYRgEzOU4f0cPZ6MaCCF29xZSGWlmbuQ==",
|
||||
"dependencies": {
|
||||
"runtime.linux-x64.Microsoft.DotNet.ILCompiler": "10.0.10"
|
||||
"runtime.linux-x64.Microsoft.DotNet.ILCompiler": "10.0.12"
|
||||
}
|
||||
},
|
||||
"runtime.linux-x64.Microsoft.DotNet.ILCompiler": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "WRjSRBfv6A6UjgjO8EQuLe9xqdICpkQx1hACUziCw4B2uGL+2jVhkFLq/G7rxRr3MGvqLo9B+nNdfIJ/5CYN7A=="
|
||||
"resolved": "10.0.12",
|
||||
"contentHash": "wAa+LAE6+Bkd+pEJvGdnQU8PZQRzvhurpQ4hzsOTpRuedIq1kunJcz86xBCBlzsc+QlVdCKVUPzEfB3uYVB7Gw=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
164
tests/SharpCompress.Test/GZip/GZipExtraFieldTests.cs
Normal file
164
tests/SharpCompress.Test/GZip/GZipExtraFieldTests.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using AwesomeAssertions;
|
||||
using SharpCompress.Archives.GZip;
|
||||
using SharpCompress.Readers.GZip;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
using SharpCompressionMode = SharpCompress.Compressors.CompressionMode;
|
||||
using SystemCompressionMode = System.IO.Compression.CompressionMode;
|
||||
|
||||
namespace SharpCompress.Test.GZip;
|
||||
|
||||
public class GZipExtraFieldTests
|
||||
{
|
||||
private static readonly byte[] EXPECTED_PAYLOAD = [.. "gzip payload"u8];
|
||||
|
||||
[Theory]
|
||||
[InlineData(32767)]
|
||||
[InlineData(32768)]
|
||||
[InlineData(65535)]
|
||||
public void GZipStream_Read_WithLargeExtraField_ShouldReturnPayload(int extraLength)
|
||||
{
|
||||
using var input = new MemoryStream(CreateGZipWithExtraField(extraLength));
|
||||
using var gzip = new SharpCompress.Compressors.Deflate.GZipStream(
|
||||
input,
|
||||
SharpCompressionMode.Decompress
|
||||
);
|
||||
using var output = new MemoryStream();
|
||||
|
||||
gzip.CopyTo(output);
|
||||
|
||||
_ = output.ToArray().Should().Equal(EXPECTED_PAYLOAD);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(32767)]
|
||||
[InlineData(32768)]
|
||||
[InlineData(65535)]
|
||||
public async ValueTask GZipStream_ReadAsync_WithLargeExtraField_ShouldReturnPayload(
|
||||
int extraLength
|
||||
)
|
||||
{
|
||||
using var rawInput = new MemoryStream(CreateGZipWithExtraField(extraLength));
|
||||
using var input = new AsyncOnlyStream(rawInput, disposeStream: false);
|
||||
await using var gzip = new SharpCompress.Compressors.Deflate.GZipStream(
|
||||
input,
|
||||
SharpCompressionMode.Decompress
|
||||
);
|
||||
using var output = new MemoryStream();
|
||||
|
||||
await gzip.CopyToAsync(output);
|
||||
|
||||
_ = output.ToArray().Should().Equal(EXPECTED_PAYLOAD);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GZipReader_Read_WithLargeExtraFieldAndName_ShouldReturnEntry()
|
||||
{
|
||||
using var input = new MemoryStream(CreateGZipWithExtraField(65535, "payload.bin"));
|
||||
using var reader = GZipReader.OpenReader(input);
|
||||
|
||||
Assert.True(reader.MoveToNextEntry());
|
||||
Assert.Equal("payload.bin", reader.Entry.Key);
|
||||
|
||||
using var output = new MemoryStream();
|
||||
reader.WriteEntryTo(output);
|
||||
|
||||
_ = output.ToArray().Should().Equal(EXPECTED_PAYLOAD);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async ValueTask GZipReader_ReadAsync_WithLargeExtraFieldAndName_ShouldReturnEntry()
|
||||
{
|
||||
using var rawInput = new MemoryStream(CreateGZipWithExtraField(65535, "payload.bin"));
|
||||
using var input = new AsyncOnlyStream(rawInput, disposeStream: false);
|
||||
await using var reader = await GZipReader.OpenAsyncReader(input);
|
||||
|
||||
Assert.True(await reader.MoveToNextEntryAsync());
|
||||
Assert.Equal("payload.bin", reader.Entry.Key);
|
||||
|
||||
using var output = new MemoryStream();
|
||||
await reader.WriteEntryToAsync(output);
|
||||
|
||||
_ = output.ToArray().Should().Equal(EXPECTED_PAYLOAD);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GZipArchive_Read_WithLargeExtraFieldAndName_ShouldReturnEntry()
|
||||
{
|
||||
using var input = new MemoryStream(CreateGZipWithExtraField(65535, "payload.bin"));
|
||||
using var archive = GZipArchive.OpenArchive(input);
|
||||
var entry = archive.Entries.Single();
|
||||
|
||||
Assert.Equal("payload.bin", entry.Key);
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var output = new MemoryStream();
|
||||
entryStream.CopyTo(output);
|
||||
|
||||
_ = output.ToArray().Should().Equal(EXPECTED_PAYLOAD);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async ValueTask GZipArchive_ReadAsync_WithLargeExtraFieldAndName_ShouldReturnEntry()
|
||||
{
|
||||
using var rawInput = new MemoryStream(CreateGZipWithExtraField(65535, "payload.bin"));
|
||||
using var input = new AsyncOnlyStream(rawInput, disposeStream: false);
|
||||
await using var archive = await GZipArchive.OpenAsyncArchive(input);
|
||||
var entry = await archive.EntriesAsync.FirstAsync();
|
||||
|
||||
Assert.Equal("payload.bin", entry.Key);
|
||||
#if NETFRAMEWORK
|
||||
using (var entryStream = await entry.OpenEntryStreamAsync())
|
||||
#else
|
||||
await using (var entryStream = await entry.OpenEntryStreamAsync())
|
||||
#endif
|
||||
{
|
||||
using var output = new MemoryStream();
|
||||
await entryStream.CopyToAsync(output);
|
||||
|
||||
_ = output.ToArray().Should().Equal(EXPECTED_PAYLOAD);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] CreateGZipWithExtraField(int extraLength, string? fileName = null)
|
||||
{
|
||||
using var compressed = new MemoryStream();
|
||||
using (
|
||||
var gzip = new System.IO.Compression.GZipStream(
|
||||
compressed,
|
||||
SystemCompressionMode.Compress,
|
||||
leaveOpen: true
|
||||
)
|
||||
)
|
||||
{
|
||||
gzip.Write(EXPECTED_PAYLOAD, 0, EXPECTED_PAYLOAD.Length);
|
||||
}
|
||||
|
||||
var original = compressed.ToArray();
|
||||
var fileNameBytes = fileName is null
|
||||
? []
|
||||
: Encoding.GetEncoding("iso-8859-1").GetBytes(fileName + "\0");
|
||||
var result = new byte[original.Length + extraLength + 2 + fileNameBytes.Length];
|
||||
Buffer.BlockCopy(original, 0, result, 0, 10);
|
||||
result[3] |= 0x04;
|
||||
if (fileNameBytes.Length > 0)
|
||||
{
|
||||
result[3] |= 0x08;
|
||||
}
|
||||
result[10] = (byte)extraLength;
|
||||
result[11] = (byte)(extraLength >> 8);
|
||||
Buffer.BlockCopy(fileNameBytes, 0, result, 12 + extraLength, fileNameBytes.Length);
|
||||
Buffer.BlockCopy(
|
||||
original,
|
||||
10,
|
||||
result,
|
||||
12 + extraLength + fileNameBytes.Length,
|
||||
original.Length - 10
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
129
tests/SharpCompress.Test/Rar/CryptKey5Tests.cs
Normal file
129
tests/SharpCompress.Test/Rar/CryptKey5Tests.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SharpCompress.Common.Rar;
|
||||
using SharpCompress.Common.Rar.Headers;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Rar;
|
||||
|
||||
public class CryptKey5Tests : TestBase
|
||||
{
|
||||
private Rar5CryptoInfo ReadCryptoInfo()
|
||||
{
|
||||
using var stream = File.OpenRead(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Rar5.encrypted_filesAndHeader.rar")
|
||||
);
|
||||
var factory = new RarHeaderFactory(StreamingMode.Seekable, ReaderOptions.ForExternalStream);
|
||||
var info = factory.ReadHeaders(stream).OfType<ArchiveCryptHeader>().First().CryptInfo;
|
||||
info.InitV = new byte[16];
|
||||
return info;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rar5_Key_SharesDerivedMaterial_AcrossEntries()
|
||||
{
|
||||
var cache = new Rar5KeyCache();
|
||||
var firstInfo = ReadCryptoInfo();
|
||||
var secondInfo = ReadCryptoInfo();
|
||||
secondInfo.InitV[0] = 1;
|
||||
var first = new CryptKey5("test", firstInfo, cache);
|
||||
var second = new CryptKey5("test", secondInfo, cache);
|
||||
using var firstTransform = first.Transformer(firstInfo.Salt);
|
||||
using var secondTransform = second.Transformer(secondInfo.Salt);
|
||||
Assert.Same(first.HashKey, second.HashKey);
|
||||
Assert.Equal(first.PswCheck, second.PswCheck);
|
||||
var firstBlock = firstTransform.TransformFinalBlock(new byte[16], 0, 16);
|
||||
var secondBlock = secondTransform.TransformFinalBlock(new byte[16], 0, 16);
|
||||
Assert.False(firstBlock.SequenceEqual(secondBlock));
|
||||
using var fresh = new CryptKey5("test", secondInfo).Transformer(secondInfo.Salt);
|
||||
Assert.Equal(fresh.TransformFinalBlock(new byte[16], 0, 16), secondBlock);
|
||||
|
||||
secondInfo.PswCheck[0] ^= 1;
|
||||
Assert.Throws<SharpCompress.Common.CryptographicException>(() =>
|
||||
new CryptKey5("test", secondInfo, cache).Transformer(secondInfo.Salt)
|
||||
);
|
||||
Assert.Throws<SharpCompress.Common.CryptographicException>(() =>
|
||||
new CryptKey5("failed", firstInfo, cache).Transformer(firstInfo.Salt)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rar5_KeyCache_MatchesInputs_AndBoundsRetention()
|
||||
{
|
||||
var cache = new Rar5KeyCache();
|
||||
var salt = new byte[16];
|
||||
var original = cache.GetKeys("test", salt, 0);
|
||||
Assert.Same(original, cache.GetKeys("test", (byte[])salt.Clone(), 0));
|
||||
Assert.NotSame(original, cache.GetKeys("other", salt, 0));
|
||||
Assert.NotSame(original, cache.GetKeys("test", salt, 1));
|
||||
salt[0] = 1;
|
||||
Assert.NotSame(original, cache.GetKeys("test", salt, 0));
|
||||
Assert.Same(original, cache.GetKeys("test", new byte[16], 0));
|
||||
salt[0] = 2;
|
||||
cache.GetKeys("test", salt, 0);
|
||||
Assert.NotSame(original, cache.GetKeys("test", new byte[16], 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rar5_Key_ReusesDerivedMaterial_WithDifferentIVs()
|
||||
{
|
||||
var info = ReadCryptoInfo();
|
||||
var key = new CryptKey5("test", info);
|
||||
using var first = key.Transformer(info.Salt);
|
||||
var firstBlock = first.TransformFinalBlock(new byte[16], 0, 16);
|
||||
var hashKey = key.HashKey;
|
||||
var passwordCheck = key.PswCheck;
|
||||
|
||||
info.InitV[0] = 1;
|
||||
using var second = key.Transformer((byte[])info.Salt.Clone());
|
||||
var secondBlock = second.TransformFinalBlock(new byte[16], 0, 16);
|
||||
Assert.Same(hashKey, key.HashKey);
|
||||
Assert.Same(passwordCheck, key.PswCheck);
|
||||
Assert.False(firstBlock.SequenceEqual(secondBlock));
|
||||
|
||||
using var fresh = new CryptKey5("test", info).Transformer(info.Salt);
|
||||
Assert.Equal(fresh.TransformFinalBlock(new byte[16], 0, 16), secondBlock);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void Rar5_Key_InvalidatesDerivedMaterial_WhenInputsChange(bool changeIterations)
|
||||
{
|
||||
var info = ReadCryptoInfo();
|
||||
var key = new CryptKey5("test", info);
|
||||
using var first = key.Transformer(info.Salt);
|
||||
var hashKey = key.HashKey;
|
||||
info.UsePswCheck = false;
|
||||
if (changeIterations)
|
||||
{
|
||||
info.LG2Count--;
|
||||
}
|
||||
else
|
||||
{
|
||||
info.Salt[0] ^= 1;
|
||||
}
|
||||
|
||||
using var second = key.Transformer(info.Salt);
|
||||
using var fresh = new CryptKey5("test", info).Transformer(info.Salt);
|
||||
Assert.NotSame(hashKey, key.HashKey);
|
||||
Assert.Equal(
|
||||
fresh.TransformFinalBlock(new byte[16], 0, 16),
|
||||
second.TransformFinalBlock(new byte[16], 0, 16)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rar5_Key_ValidatesPasswordCheck_OnCacheHit()
|
||||
{
|
||||
var info = ReadCryptoInfo();
|
||||
var key = new CryptKey5("test", info);
|
||||
using var first = key.Transformer(info.Salt);
|
||||
info.PswCheck[0] ^= 1;
|
||||
Assert.Throws<SharpCompress.Common.CryptographicException>(() =>
|
||||
key.Transformer(info.Salt)
|
||||
);
|
||||
}
|
||||
}
|
||||
365
tests/SharpCompress.Test/Rar/RarCommentTests.cs
Normal file
365
tests/SharpCompress.Test/Rar/RarCommentTests.cs
Normal file
@@ -0,0 +1,365 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.Rar;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Rar;
|
||||
using SharpCompress.Compressors.Rar;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.Rar;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Rar;
|
||||
|
||||
public class RarCommentTests : TestBase
|
||||
{
|
||||
public static IEnumerable<object[]> Comments()
|
||||
{
|
||||
// Generated with RAR 7.22: rar a -ma5 -m0 -qo- -zcomment.txt [-hptest|-ptest] archive.rar payload.txt.
|
||||
foreach (var useAsync in new[] { false, true })
|
||||
{
|
||||
yield return ["Rar5.comment.encrypted15.rar", "123456789012345", useAsync];
|
||||
yield return ["Rar5.comment.encrypted16.rar", "1234567890123456", useAsync];
|
||||
yield return ["Rar5.comment.encrypted17.rar", "123456789012345é", useAsync];
|
||||
yield return ["Rar5.comment.plain.rar", "123456789012345é", useAsync];
|
||||
yield return ["Rar5.comment.filesOnly.rar", "123456789012345é", useAsync];
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Comments))]
|
||||
public async Task Rar5_Comment_Archive(string filename, string expected, bool useAsync)
|
||||
{
|
||||
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename));
|
||||
var options = ReaderOptions.ForExternalStream.WithPassword("test");
|
||||
using var output = new MemoryStream();
|
||||
if (useAsync)
|
||||
{
|
||||
await using var archive = await RarArchive.OpenAsyncArchive(stream, options);
|
||||
await foreach (var entry in archive.EntriesAsync)
|
||||
{
|
||||
Assert.Equal("payload.txt", entry.Key);
|
||||
await entry.WriteToAsync(output, cancellationToken: CancellationToken.None);
|
||||
}
|
||||
await foreach (var volume in archive.VolumesAsync)
|
||||
{
|
||||
Assert.Equal(expected, ((RarVolume)volume).Comment);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using var archive = RarArchive.OpenArchive(stream, options);
|
||||
var entry = Assert.Single(archive.Entries);
|
||||
Assert.Equal("payload.txt", entry.Key);
|
||||
entry.WriteTo(output);
|
||||
Assert.Equal(expected, archive.Volumes.OfType<RarVolume>().First().Comment);
|
||||
}
|
||||
Assert.Equal("hello from RAR!\n", Encoding.UTF8.GetString(output.ToArray()));
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, false)]
|
||||
[InlineData(null, true)]
|
||||
[InlineData("wrong", false)]
|
||||
[InlineData("wrong", true)]
|
||||
public async Task Rar5_Comment_Rejects_Invalid_Password(string? password, bool useAsync)
|
||||
{
|
||||
using var stream = File.OpenRead(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Rar5.comment.encrypted17.rar")
|
||||
);
|
||||
var options = ReaderOptions.ForExternalStream with { Password = password };
|
||||
if (useAsync)
|
||||
{
|
||||
await Assert.ThrowsAsync<SharpCompress.Common.CryptographicException>(async () =>
|
||||
{
|
||||
await using var archive = await RarArchive.OpenAsyncArchive(stream, options);
|
||||
await foreach (var entry in archive.EntriesAsync)
|
||||
{
|
||||
_ = entry.Key;
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Throws<SharpCompress.Common.CryptographicException>(() =>
|
||||
{
|
||||
using var archive = RarArchive.OpenArchive(stream, options);
|
||||
_ = archive.Entries.ToArray();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Comments))]
|
||||
public async Task Rar5_Comment_Reader(string filename, string expected, bool useAsync)
|
||||
{
|
||||
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename));
|
||||
using var forwardOnly = new ForwardOnlyStream(stream);
|
||||
var options = ReaderOptions.ForExternalStream.WithPassword("test");
|
||||
using var output = new MemoryStream();
|
||||
if (useAsync)
|
||||
{
|
||||
await using var reader = await ReaderFactory.OpenAsyncReader(forwardOnly, options);
|
||||
Assert.True(await reader.MoveToNextEntryAsync(CancellationToken.None));
|
||||
Assert.Equal(expected, ((RarReader)reader).Volume!.Comment);
|
||||
await reader.WriteEntryToAsync(output, CancellationToken.None);
|
||||
Assert.False(await reader.MoveToNextEntryAsync(CancellationToken.None));
|
||||
}
|
||||
else
|
||||
{
|
||||
using var reader = RarReader.OpenReader(forwardOnly, options);
|
||||
Assert.True(reader.MoveToNextEntry());
|
||||
Assert.Equal(expected, ((RarReader)reader).Volume!.Comment);
|
||||
reader.WriteEntryTo(output);
|
||||
Assert.False(reader.MoveToNextEntry());
|
||||
}
|
||||
Assert.Equal("hello from RAR!\n", Encoding.UTF8.GetString(output.ToArray()));
|
||||
Assert.True(forwardOnly.CanRead);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Rar5_Comment_Volume_Async_Cancellation()
|
||||
{
|
||||
using var stream = File.OpenRead(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Rar5.comment.encrypted17.rar")
|
||||
);
|
||||
using var volume = new StreamRarArchiveVolume(
|
||||
stream,
|
||||
ReaderOptions.ForExternalStream.WithPassword("test"),
|
||||
0
|
||||
);
|
||||
using var cancellation = new CancellationTokenSource();
|
||||
cancellation.Cancel();
|
||||
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
|
||||
{
|
||||
await foreach (var part in volume.GetVolumeFilePartsAsync(cancellation.Token))
|
||||
{
|
||||
_ = part.FileHeader;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> InvalidCommentSizes()
|
||||
{
|
||||
foreach (var useAsync in new[] { false, true })
|
||||
{
|
||||
foreach (var useReader in new[] { false, true })
|
||||
{
|
||||
foreach (
|
||||
var size in new long[] { 16 * 1024 * 1024 + 1, int.MaxValue, long.MaxValue }
|
||||
)
|
||||
{
|
||||
yield return [size, useAsync, useReader];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(InvalidCommentSizes))]
|
||||
public async Task Rar5_Comment_Rejects_Oversized_Data(long size, bool useAsync, bool useReader)
|
||||
{
|
||||
using var stream = CreateCommentArchive(size, 0, [1]);
|
||||
var exception = await Assert.ThrowsAsync<InvalidFormatException>(() =>
|
||||
ReadCommentArchive(stream, useAsync, useReader)
|
||||
);
|
||||
Assert.Contains("16 MiB", exception.Message);
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public async Task Rar5_Comment_Allows_Size_Limit(bool useAsync, bool useReader)
|
||||
{
|
||||
var data = new byte[16 * 1024 * 1024];
|
||||
data[0] = (byte)'a';
|
||||
using var stream = CreateCommentArchive(data.Length, 0, data);
|
||||
Assert.Equal("a", await ReadCommentArchive(stream, useAsync, useReader));
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, false)]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(true, true)]
|
||||
public async Task Rar5_Comment_Rejects_Truncated_Data(bool useAsync, bool useReader)
|
||||
{
|
||||
using var stream = CreateCommentArchive(10, 0, [1, 2, 3]);
|
||||
await Assert.ThrowsAsync<EndOfStreamException>(() =>
|
||||
ReadCommentArchive(stream, useAsync, useReader)
|
||||
);
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> UnsupportedCommentMethods()
|
||||
{
|
||||
foreach (var useAsync in new[] { false, true })
|
||||
{
|
||||
foreach (var useReader in new[] { false, true })
|
||||
{
|
||||
foreach (var method in Enumerable.Range(1, 7))
|
||||
{
|
||||
yield return [method, useAsync, useReader];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(UnsupportedCommentMethods))]
|
||||
public async Task Rar5_Comment_Skips_Nonzero_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]);
|
||||
Assert.Null(await ReadCommentArchive(stream, useAsync, useReader));
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
private static async Task<string?> ReadCommentArchive(
|
||||
Stream stream,
|
||||
bool useAsync,
|
||||
bool useReader
|
||||
)
|
||||
{
|
||||
var options = ReaderOptions.ForExternalStream;
|
||||
if (useReader)
|
||||
{
|
||||
using var forwardOnly = new ForwardOnlyStream(
|
||||
SharpCompressStream.CreateNonDisposing(stream)
|
||||
);
|
||||
using var output = new MemoryStream();
|
||||
string? comment;
|
||||
if (useAsync)
|
||||
{
|
||||
await using var reader = await ReaderFactory.OpenAsyncReader(
|
||||
forwardOnly,
|
||||
options,
|
||||
CancellationToken.None
|
||||
);
|
||||
Assert.True(await reader.MoveToNextEntryAsync(CancellationToken.None));
|
||||
Assert.Equal("payload.txt", reader.Entry.Key);
|
||||
comment = ((RarReader)reader).Volume!.Comment;
|
||||
await reader.WriteEntryToAsync(output, CancellationToken.None);
|
||||
Assert.False(await reader.MoveToNextEntryAsync(CancellationToken.None));
|
||||
}
|
||||
else
|
||||
{
|
||||
using var reader = RarReader.OpenReader(forwardOnly, options);
|
||||
Assert.True(reader.MoveToNextEntry());
|
||||
Assert.Equal("payload.txt", reader.Entry.Key);
|
||||
comment = ((RarReader)reader).Volume!.Comment;
|
||||
reader.WriteEntryTo(output);
|
||||
Assert.False(reader.MoveToNextEntry());
|
||||
}
|
||||
Assert.Equal("hello from RAR!\n", Encoding.UTF8.GetString(output.ToArray()));
|
||||
return comment;
|
||||
}
|
||||
|
||||
using var extracted = new MemoryStream();
|
||||
if (useAsync)
|
||||
{
|
||||
await using var archive = await RarArchive.OpenAsyncArchive(
|
||||
stream,
|
||||
options,
|
||||
CancellationToken.None
|
||||
);
|
||||
var count = 0;
|
||||
await foreach (var entry in archive.EntriesAsync)
|
||||
{
|
||||
Assert.Equal("payload.txt", entry.Key);
|
||||
await entry.WriteToAsync(extracted, cancellationToken: CancellationToken.None);
|
||||
count++;
|
||||
}
|
||||
Assert.Equal(1, count);
|
||||
Assert.Equal("hello from RAR!\n", Encoding.UTF8.GetString(extracted.ToArray()));
|
||||
await foreach (var volume in archive.VolumesAsync)
|
||||
{
|
||||
return ((RarVolume)volume).Comment;
|
||||
}
|
||||
throw new InvalidOperationException("Expected a RAR volume.");
|
||||
}
|
||||
|
||||
using var syncArchive = RarArchive.OpenArchive(stream, options);
|
||||
var syncEntry = Assert.Single(syncArchive.Entries);
|
||||
Assert.Equal("payload.txt", syncEntry.Key);
|
||||
syncEntry.WriteTo(extracted);
|
||||
Assert.Equal("hello from RAR!\n", Encoding.UTF8.GetString(extracted.ToArray()));
|
||||
return syncArchive.Volumes.OfType<RarVolume>().First().Comment;
|
||||
}
|
||||
|
||||
private static MemoryStream CreateCommentArchive(long size, int method, byte[] commentData)
|
||||
{
|
||||
// Build small headers with valid CRCs so malformed sizes reach comment validation.
|
||||
var archive = new MemoryStream();
|
||||
using var writer = new BinaryWriter(archive, Encoding.UTF8, leaveOpen: true);
|
||||
writer.Write(new byte[] { 0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00 });
|
||||
WriteHeader(writer, [1, 0, 0]);
|
||||
|
||||
using var body = new MemoryStream();
|
||||
using var header = new BinaryWriter(body, Encoding.UTF8, leaveOpen: true);
|
||||
WriteVInts(
|
||||
header,
|
||||
3,
|
||||
2,
|
||||
commentData.Length,
|
||||
size == long.MaxValue ? 8 : 0,
|
||||
size,
|
||||
0,
|
||||
method << 7,
|
||||
1,
|
||||
3
|
||||
);
|
||||
header.Write(Encoding.UTF8.GetBytes("CMT"));
|
||||
WriteHeader(writer, body.ToArray());
|
||||
writer.Write(commentData);
|
||||
|
||||
body.SetLength(0);
|
||||
var payload = Encoding.UTF8.GetBytes("hello from RAR!\n");
|
||||
WriteVInts(header, 2, 2, payload.Length, 4, payload.Length, 0);
|
||||
header.Write(~RarCRC.CheckCrc(uint.MaxValue, payload, 0, payload.Length));
|
||||
WriteVInts(header, 0, 1, 11);
|
||||
header.Write(Encoding.UTF8.GetBytes("payload.txt"));
|
||||
WriteHeader(writer, body.ToArray());
|
||||
writer.Write(payload);
|
||||
WriteHeader(writer, [5, 0, 0]);
|
||||
archive.Position = 0;
|
||||
return archive;
|
||||
}
|
||||
|
||||
private static void WriteHeader(BinaryWriter writer, byte[] body)
|
||||
{
|
||||
using var bytes = new MemoryStream();
|
||||
using var header = new BinaryWriter(bytes);
|
||||
WriteVInts(header, body.Length);
|
||||
header.Write(body);
|
||||
var data = bytes.ToArray();
|
||||
writer.Write(~RarCRC.CheckCrc(uint.MaxValue, data, 0, data.Length));
|
||||
writer.Write(data);
|
||||
}
|
||||
|
||||
private static void WriteVInts(BinaryWriter writer, params long[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
var remaining = (ulong)value;
|
||||
while (remaining >= 0x80)
|
||||
{
|
||||
writer.Write((byte)((remaining & 0x7f) | 0x80));
|
||||
remaining >>= 7;
|
||||
}
|
||||
writer.Write((byte)remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
140
tests/SharpCompress.Test/Rar/RarEncryptedHeaderScanTests.cs
Normal file
140
tests/SharpCompress.Test/Rar/RarEncryptedHeaderScanTests.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives.Rar;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Rar.Headers;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Rar;
|
||||
|
||||
public class RarEncryptedHeaderScanTests : TestBase
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Rar.encrypted_filesAndHeader.rar", false)]
|
||||
[InlineData("Rar.encrypted_filesAndHeader.rar", true)]
|
||||
[InlineData("Rar5.encrypted_filesAndHeader.rar", false)]
|
||||
[InlineData("Rar5.encrypted_filesAndHeader.rar", true)]
|
||||
public async Task Rar_EncryptedHeaders_Rescan(string filename, bool useAsync)
|
||||
{
|
||||
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, filename));
|
||||
var factory = new RarHeaderFactory(
|
||||
StreamingMode.Seekable,
|
||||
ReaderOptions.ForExternalStream.WithPassword("test")
|
||||
);
|
||||
var expected = await ReadNames(false);
|
||||
Assert.Equal(6, expected.Count);
|
||||
stream.Position = 0;
|
||||
Assert.Single(await ReadNames(true));
|
||||
stream.Position = 0;
|
||||
Assert.Equal(expected, await ReadNames(false));
|
||||
|
||||
async Task<List<string>> ReadNames(bool stopAfterFirst)
|
||||
{
|
||||
var names = new List<string>();
|
||||
if (useAsync)
|
||||
{
|
||||
await foreach (var header in factory.ReadHeadersAsync(stream))
|
||||
{
|
||||
if (header is FileHeader file && header.HeaderType == HeaderType.File)
|
||||
{
|
||||
Assert.NotNull(file.FileName);
|
||||
names.Add(file.FileName);
|
||||
if (stopAfterFirst)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var header in factory.ReadHeaders(stream))
|
||||
{
|
||||
if (header is FileHeader file && header.HeaderType == HeaderType.File)
|
||||
{
|
||||
Assert.NotNull(file.FileName);
|
||||
names.Add(file.FileName);
|
||||
if (stopAfterFirst)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void Rar5_Solid_EncryptedHeaders_ExtractAll(bool readMetadataFirst)
|
||||
{
|
||||
using var stream = File.OpenRead(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Rar5.solid.encrypted_headers.rar")
|
||||
);
|
||||
using var archive = RarArchive.OpenArchive(
|
||||
stream,
|
||||
ReaderOptions.ForExternalStream.WithPassword("test")
|
||||
);
|
||||
if (readMetadataFirst)
|
||||
{
|
||||
Assert.True(archive.IsSolid);
|
||||
Assert.Equal(3, archive.Entries.Count());
|
||||
}
|
||||
using var reader = archive.ExtractAllEntries();
|
||||
var count = 0;
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
Assert.Equal($"f{count:D3}.txt", reader.Entry.Key);
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
using var textReader = new StreamReader(entryStream);
|
||||
Assert.Equal("Solid encrypted header regression test.\n", textReader.ReadToEnd());
|
||||
count++;
|
||||
}
|
||||
Assert.Equal(3, count);
|
||||
Assert.Equal(3, archive.Entries.Count());
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Rar5_Solid_EncryptedHeaders_ExtractAll_Async(bool readMetadataFirst)
|
||||
{
|
||||
using var stream = File.OpenRead(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Rar5.solid.encrypted_headers.rar")
|
||||
);
|
||||
await using var archive = await RarArchive.OpenAsyncArchive(
|
||||
stream,
|
||||
ReaderOptions.ForExternalStream.WithPassword("test"),
|
||||
CancellationToken.None
|
||||
);
|
||||
if (readMetadataFirst)
|
||||
{
|
||||
Assert.True(await archive.IsSolidAsync());
|
||||
Assert.Equal(3, (await archive.EntriesAsync.ToListAsync()).Count);
|
||||
}
|
||||
await using var reader = await archive.ExtractAllEntriesAsync();
|
||||
var count = 0;
|
||||
while (await reader.MoveToNextEntryAsync(CancellationToken.None))
|
||||
{
|
||||
Assert.Equal($"f{count:D3}.txt", reader.Entry.Key);
|
||||
using var entryStream = await reader.OpenEntryStreamAsync(CancellationToken.None);
|
||||
using var textReader = new StreamReader(entryStream);
|
||||
Assert.Equal(
|
||||
"Solid encrypted header regression test.\n",
|
||||
await textReader.ReadToEndAsync()
|
||||
);
|
||||
count++;
|
||||
}
|
||||
Assert.Equal(3, count);
|
||||
Assert.Equal(3, (await archive.EntriesAsync.ToListAsync()).Count);
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Rar.Headers;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
@@ -52,4 +54,64 @@ public class RarHeaderFactoryTest : TestBase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Rar5_ReadHeaders_WithCachedKey(bool useAsync)
|
||||
{
|
||||
var options = ReaderOptions.ForExternalStream.WithPassword("test");
|
||||
Assert.Equal(6, await CountEncryptedFileHeaders(useAsync, options, false));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task Rar5_ReadHeaders_RejectChangedPassword(bool useAsync)
|
||||
{
|
||||
var options = ReaderOptions.ForExternalStream.WithPassword("test");
|
||||
await Assert.ThrowsAsync<CryptographicException>(async () =>
|
||||
await CountEncryptedFileHeaders(useAsync, options, true)
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<int> CountEncryptedFileHeaders(
|
||||
bool useAsync,
|
||||
ReaderOptions options,
|
||||
bool changePassword
|
||||
)
|
||||
{
|
||||
using var stream = File.OpenRead(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Rar5.encrypted_filesAndHeader.rar")
|
||||
);
|
||||
var factory = new RarHeaderFactory(StreamingMode.Seekable, options);
|
||||
var count = 0;
|
||||
if (useAsync)
|
||||
{
|
||||
await foreach (var header in factory.ReadHeadersAsync(stream))
|
||||
{
|
||||
CountHeader(header);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var header in factory.ReadHeaders(stream))
|
||||
{
|
||||
CountHeader(header);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
|
||||
void CountHeader(IRarHeader header)
|
||||
{
|
||||
if (header.HeaderType == HeaderType.File)
|
||||
{
|
||||
count++;
|
||||
if (changePassword)
|
||||
{
|
||||
options.Password = "failed";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.SevenZip;
|
||||
using SharpCompress.Crypto;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.SevenZip;
|
||||
|
||||
public class SevenZipHeaderValidationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0, 64 * 1024 * 1024, 0)]
|
||||
[InlineData(0, 256 * 1024 * 1024, 0)]
|
||||
[InlineData(0, 2, 1)]
|
||||
[InlineData(1, 2, 2)]
|
||||
[InlineData(2, 1, 2)]
|
||||
public void SevenZipArchive_TruncatedNextHeader_ThrowsBeforeReadingHeader(
|
||||
int offset,
|
||||
int size,
|
||||
int available
|
||||
)
|
||||
{
|
||||
using var stream = CreateArchive(offset, size, available);
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
|
||||
var exception = Assert.Throws<IncompleteArchiveException>(() => archive.Entries.ToList());
|
||||
|
||||
Assert.Equal("Next header extends beyond the end of stream.", exception.Message);
|
||||
Assert.Equal(32L, stream.Position);
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 64 * 1024 * 1024, 0)]
|
||||
[InlineData(0, 256 * 1024 * 1024, 0)]
|
||||
[InlineData(0, 2, 1)]
|
||||
[InlineData(1, 2, 2)]
|
||||
[InlineData(2, 1, 2)]
|
||||
public async Task SevenZipArchive_TruncatedNextHeader_AsyncThrowsBeforeReadingHeader(
|
||||
int offset,
|
||||
int size,
|
||||
int available
|
||||
)
|
||||
{
|
||||
using var stream = CreateArchive(offset, size, available);
|
||||
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
asyncStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<IncompleteArchiveException>(async () =>
|
||||
{
|
||||
await foreach (
|
||||
var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None)
|
||||
) { }
|
||||
});
|
||||
|
||||
Assert.Equal("Next header extends beyond the end of stream.", exception.Message);
|
||||
Assert.Equal(32L, stream.Position);
|
||||
Assert.True(stream.CanRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 0, 0)]
|
||||
[InlineData(0, 2, 2)]
|
||||
[InlineData(3, 2, 5)]
|
||||
public void SevenZipArchive_NextHeaderAtEnd_IsValid(int offset, int size, int available)
|
||||
{
|
||||
using var stream = CreateArchive(offset, size, available);
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
|
||||
Assert.Empty(archive.Entries);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 0, 0)]
|
||||
[InlineData(0, 2, 2)]
|
||||
[InlineData(3, 2, 5)]
|
||||
public async Task SevenZipArchive_NextHeaderAtEnd_AsyncIsValid(
|
||||
int offset,
|
||||
int size,
|
||||
int available
|
||||
)
|
||||
{
|
||||
using var stream = CreateArchive(offset, size, available);
|
||||
using var asyncStream = new AsyncOnlyStream(stream, disposeStream: false);
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(
|
||||
asyncStream,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
|
||||
var count = 0;
|
||||
await foreach (var entry in archive.EntriesAsync.WithCancellation(CancellationToken.None))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
Assert.Equal(0, count);
|
||||
}
|
||||
|
||||
private static MemoryStream CreateArchive(int offset, int size, int available)
|
||||
{
|
||||
// A minimal uncompressed next header: kHeader followed by kEnd.
|
||||
byte[] header = [1, 0];
|
||||
var crc = Crc32Stream.Compute(
|
||||
Crc32Stream.DEFAULT_POLYNOMIAL,
|
||||
Crc32Stream.DEFAULT_SEED,
|
||||
header
|
||||
);
|
||||
var stream = new MemoryStream();
|
||||
SevenZipSignatureHeaderWriter.WriteFinal(stream, (ulong)offset, (ulong)size, crc);
|
||||
stream.SetLength(32 + available);
|
||||
if (available >= offset + header.Length)
|
||||
{
|
||||
stream.Position = 32 + offset;
|
||||
stream.Write(header, 0, header.Length);
|
||||
}
|
||||
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
@@ -148,6 +148,17 @@ public class ZipArchiveTests : ArchiveTests
|
||||
//uses first part to search for all parts and compares against this array
|
||||
ArchiveGetParts(new[] { "WinZip26.nocomp.multi.zipx", "WinZip26.nocomp.multi.zx01" });
|
||||
|
||||
[Fact]
|
||||
public void Zip_GetParts_UpperCaseZip()
|
||||
{
|
||||
var zip = Path.Combine(SCRATCH2_FILES_PATH, "MULTI.ZIP");
|
||||
var z01 = Path.Combine(SCRATCH2_FILES_PATH, "MULTI.z01");
|
||||
File.Copy(Path.Combine(TEST_ARCHIVES_PATH, "Infozip.nocomp.multi.zip"), zip);
|
||||
File.Copy(Path.Combine(TEST_ARCHIVES_PATH, "Infozip.nocomp.multi.z01"), z01);
|
||||
|
||||
Assert.Equal(new[] { zip, z01 }, ArchiveFactory.GetFileParts(zip));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Zip_GetPartsSplit() =>
|
||||
//uses first part to search for all parts and compares against this array
|
||||
|
||||
BIN
tests/TestArchives/Archives/Rar5.comment.encrypted15.rar
Normal file
BIN
tests/TestArchives/Archives/Rar5.comment.encrypted15.rar
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Rar5.comment.encrypted16.rar
Normal file
BIN
tests/TestArchives/Archives/Rar5.comment.encrypted16.rar
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Rar5.comment.encrypted17.rar
Normal file
BIN
tests/TestArchives/Archives/Rar5.comment.encrypted17.rar
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Rar5.comment.filesOnly.rar
Normal file
BIN
tests/TestArchives/Archives/Rar5.comment.filesOnly.rar
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Rar5.comment.plain.rar
Normal file
BIN
tests/TestArchives/Archives/Rar5.comment.plain.rar
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Rar5.solid.encrypted_headers.rar
Normal file
BIN
tests/TestArchives/Archives/Rar5.solid.encrypted_headers.rar
Normal file
Binary file not shown.
Reference in New Issue
Block a user