diff --git a/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs index 640e2b90..a6108b32 100644 --- a/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs +++ b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs @@ -14,6 +14,7 @@ public partial class RarHeaderFactory { public async IAsyncEnumerable ReadHeadersAsync(Stream stream) { + ResetHeaderScanState(); var markHeader = await MarkHeader .ReadAsync( stream, diff --git a/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs index 67ee8c69..27ae9db6 100644 --- a/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs +++ b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.cs @@ -39,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 ReadHeaders(Stream stream) { + ResetHeaderScanState(); var markHeader = MarkHeader.Read(stream, Options.LeaveStreamOpen, Options.LookForHeader); _isRar5 = markHeader.IsRar5; yield return markHeader; diff --git a/tests/SharpCompress.Test/Rar/RarEncryptedHeaderScanTests.cs b/tests/SharpCompress.Test/Rar/RarEncryptedHeaderScanTests.cs new file mode 100644 index 00000000..dddb63a2 --- /dev/null +++ b/tests/SharpCompress.Test/Rar/RarEncryptedHeaderScanTests.cs @@ -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> ReadNames(bool stopAfterFirst) + { + var names = new List(); + 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); + await 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); + } +} diff --git a/tests/TestArchives/Archives/Rar5.solid.encrypted_headers.rar b/tests/TestArchives/Archives/Rar5.solid.encrypted_headers.rar new file mode 100644 index 00000000..a9c035a3 Binary files /dev/null and b/tests/TestArchives/Archives/Rar5.solid.encrypted_headers.rar differ