Files
sharpcompress/tests/SharpCompress.Test/SevenZip/SevenZipSolidCacheTests.cs

432 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Common;
using SharpCompress.Readers;
using Xunit;
namespace SharpCompress.Test.SevenZip;
/// <summary>
/// A solid archive is decoded by a single shared, forward-only stream. These tests cover the
/// bookkeeping of that shared stream: entries that are not read completely must not make the
/// following entries start at the wrong offset.
/// </summary>
public class SevenZipSolidCacheTests : TestBase
{
private const string SolidArchive = "7Zip.solid.7z";
private const string SingleBlockSolidArchive = "7Zip.solid.1block.7z";
private static string PathOf(string archiveName) =>
Path.Combine(TEST_ARCHIVES_PATH, archiveName);
private static List<IArchiveEntry> FileEntries(IArchive archive) =>
archive.Entries.Where(e => !e.IsDirectory).ToList();
private static async Task<List<IArchiveEntry>> FileEntriesAsync(IAsyncArchive archive) =>
(
await archive
.EntriesAsync.Where(e => !e.IsDirectory)
.ToListAsync()
.ConfigureAwait(false)
).ToList();
/// <summary>
/// Reads a single entry from its own archive instance so that the expected content never
/// depends on the shared folder stream.
/// </summary>
private static byte[] ExpectedContent(string archiveName, int entryIndex)
{
using var archive = SevenZipArchive.OpenArchive(PathOf(archiveName));
var entry = FileEntries(archive)[entryIndex];
using var stream = entry.OpenEntryStream();
using var content = new MemoryStream();
stream.CopyTo(content);
return content.ToArray();
}
private static async Task<byte[]> ExpectedContentAsync(string archiveName, int entryIndex)
{
await using var archive = await SevenZipArchive
.OpenAsyncArchive(PathOf(archiveName))
.ConfigureAwait(false);
var entry = (await FileEntriesAsync(archive).ConfigureAwait(false))[entryIndex];
var stream = await entry.OpenEntryStreamAsync(CancellationToken.None).ConfigureAwait(false);
await using var scope = stream.DisposeAsyncScope().ConfigureAwait(false);
using var content = new MemoryStream();
await stream
.CopyToAsync(content, Constants.BufferSize, CancellationToken.None)
.ConfigureAwait(false);
return content.ToArray();
}
private static byte[] ReadAll(IArchiveEntry entry)
{
using var stream = entry.OpenEntryStream();
using var content = new MemoryStream();
stream.CopyTo(content);
return content.ToArray();
}
private static async Task<byte[]> ReadAllAsync(IArchiveEntry entry)
{
var stream = await entry.OpenEntryStreamAsync(CancellationToken.None).ConfigureAwait(false);
await using var scope = stream.DisposeAsyncScope().ConfigureAwait(false);
using var content = new MemoryStream();
await stream
.CopyToAsync(content, Constants.BufferSize, CancellationToken.None)
.ConfigureAwait(false);
return content.ToArray();
}
private static void ReadPartial(IArchiveEntry entry, int byteCount)
{
using var stream = entry.OpenEntryStream();
var buffer = new byte[byteCount];
Assert.Equal(byteCount, stream.Read(buffer, 0, buffer.Length));
}
private static async Task ReadPartialAsync(IArchiveEntry entry, int byteCount)
{
var stream = await entry.OpenEntryStreamAsync(CancellationToken.None).ConfigureAwait(false);
await using var scope = stream.DisposeAsyncScope().ConfigureAwait(false);
var buffer = new byte[byteCount];
Assert.Equal(
byteCount,
await stream
.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None)
.ConfigureAwait(false)
);
}
private static object? GetField(object instance, string name) =>
instance
.GetType()
.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!
.GetValue(instance);
private static object? CachedFolderStream(object archive) =>
GetField(GetField(archive, "_database")!, "_cachedFolderStream");
[Theory]
[InlineData(SolidArchive)]
[InlineData(SingleBlockSolidArchive)]
public void PartialRead_ThenNextEntry_ReturnsEntryContent(string archiveName)
{
var expected = ExpectedContent(archiveName, 1);
using var archive = SevenZipArchive.OpenArchive(PathOf(archiveName));
var entries = FileEntries(archive);
Assert.True(entries.Count >= 2);
ReadPartial(entries[0], 16);
Assert.Equal(expected, ReadAll(entries[1]));
}
[Theory]
[InlineData(SolidArchive)]
[InlineData(SingleBlockSolidArchive)]
public void ZeroRead_ThenNextEntry_ReturnsEntryContent(string archiveName)
{
var expected = ExpectedContent(archiveName, 1);
using var archive = SevenZipArchive.OpenArchive(PathOf(archiveName));
var entries = FileEntries(archive);
Assert.True(entries.Count >= 2);
using (entries[0].OpenEntryStream()) { }
Assert.Equal(expected, ReadAll(entries[1]));
}
[Theory]
[InlineData(SolidArchive)]
[InlineData(SingleBlockSolidArchive)]
public void PartialRead_ThenEarlierEntry_ReturnsEntryContent(string archiveName)
{
var expected = ExpectedContent(archiveName, 0);
using var archive = SevenZipArchive.OpenArchive(PathOf(archiveName));
var entries = FileEntries(archive);
Assert.True(entries.Count >= 2);
ReadPartial(entries[1], 16);
Assert.Equal(expected, ReadAll(entries[0]));
}
[Fact]
public void PartialRead_ThenLaterEntry_ReturnsEntryContent()
{
const string archiveName = "7Zip.delta.7z";
var expected = ExpectedContent(archiveName, 2);
using var archive = SevenZipArchive.OpenArchive(PathOf(archiveName));
var entries = FileEntries(archive);
Assert.True(entries.Count >= 3);
Assert.Same(
((SevenZipArchiveEntry)entries[0]).FilePart.Folder,
((SevenZipArchiveEntry)entries[2]).FilePart.Folder
);
ReadPartial(entries[0], 16);
Assert.Equal(expected, ReadAll(entries[2]));
}
[Fact]
public async Task PartialRead_ThenSkippedEntryAsync_ReusesFolder()
{
const string archiveName = "7Zip.delta.7z";
await using var archive = await SevenZipArchive.OpenAsyncArchive(PathOf(archiveName));
var entries = await FileEntriesAsync(archive);
Assert.Same(
((SevenZipArchiveEntry)entries[0]).FilePart.Folder,
((SevenZipArchiveEntry)entries[2]).FilePart.Folder
);
await ReadPartialAsync(entries[0], 16);
var decoder = CachedFolderStream(archive);
Assert.Equal(ExpectedContent(archiveName, 2), await ReadAllAsync(entries[2]));
Assert.Same(decoder, CachedFolderStream(archive));
}
[Fact]
public async Task FullyReadEntriesAsync_ReuseFolder()
{
await using var archive = await SevenZipArchive.OpenAsyncArchive(PathOf(SolidArchive));
var entries = await FileEntriesAsync(archive);
await ReadAllAsync(entries[0]);
var decoder = CachedFolderStream(archive);
Assert.Equal(ExpectedContent(SolidArchive, 1), await ReadAllAsync(entries[1]));
Assert.Same(decoder, CachedFolderStream(archive));
}
[Theory]
[InlineData(SolidArchive)]
[InlineData(SingleBlockSolidArchive)]
public async Task PartialRead_ThenNextEntryAsync_ReturnsEntryContent(string archiveName)
{
var expected = await ExpectedContentAsync(archiveName, 1);
await using var archive = await SevenZipArchive
.OpenAsyncArchive(PathOf(archiveName))
.ConfigureAwait(false);
var entries = await FileEntriesAsync(archive).ConfigureAwait(false);
Assert.True(entries.Count >= 2);
await ReadPartialAsync(entries[0], 16).ConfigureAwait(false);
Assert.Equal(expected, await ReadAllAsync(entries[1]).ConfigureAwait(false));
}
[Theory]
[InlineData(SolidArchive)]
[InlineData(SingleBlockSolidArchive)]
public async Task ZeroRead_ThenNextEntryAsync_ReturnsEntryContent(string archiveName)
{
var expected = await ExpectedContentAsync(archiveName, 1);
await using var archive = await SevenZipArchive
.OpenAsyncArchive(PathOf(archiveName))
.ConfigureAwait(false);
var entries = await FileEntriesAsync(archive).ConfigureAwait(false);
Assert.True(entries.Count >= 2);
var first = await entries[0]
.OpenEntryStreamAsync(CancellationToken.None)
.ConfigureAwait(false);
await using (first.DisposeAsyncScope()) { }
Assert.Equal(expected, await ReadAllAsync(entries[1]).ConfigureAwait(false));
}
[Theory]
[InlineData(SolidArchive)]
[InlineData(SingleBlockSolidArchive)]
public async Task PartialRead_ThenEarlierEntryAsync_ReturnsEntryContent(string archiveName)
{
var expected = await ExpectedContentAsync(archiveName, 0);
await using var archive = await SevenZipArchive
.OpenAsyncArchive(PathOf(archiveName))
.ConfigureAwait(false);
var entries = await FileEntriesAsync(archive).ConfigureAwait(false);
Assert.True(entries.Count >= 2);
await ReadPartialAsync(entries[1], 16).ConfigureAwait(false);
Assert.Equal(expected, await ReadAllAsync(entries[0]).ConfigureAwait(false));
}
[Theory]
[InlineData(true, 0)]
[InlineData(true, 16)]
[InlineData(true, -1)]
[InlineData(false, 0)]
[InlineData(false, 16)]
[InlineData(false, -1)]
public async Task AlternatingEntryReadModes_ReturnEntryContent(bool syncFirst, int count)
{
var expected = ExpectedContent(SolidArchive, 1);
await using var archive = await SevenZipArchive.OpenAsyncArchive(PathOf(SolidArchive));
var entries = await FileEntriesAsync(archive);
if (syncFirst)
{
if (count < 0)
{
ReadAll(entries[0]);
}
else
{
ReadPartial(entries[0], count);
}
var decoder = CachedFolderStream(archive);
Assert.Equal(expected, await ReadAllAsync(entries[1]));
if (count != 0)
{
Assert.NotSame(decoder, CachedFolderStream(archive));
}
}
else
{
if (count < 0)
{
await ReadAllAsync(entries[0]);
}
else
{
await ReadPartialAsync(entries[0], count);
}
var decoder = CachedFolderStream(archive);
Assert.Equal(expected, ReadAll(entries[1]));
if (count != 0)
{
Assert.NotSame(decoder, CachedFolderStream(archive));
}
}
}
[Fact]
public async Task CancellationAfterSourceProgress_RestartsCachedDecoder()
{
using var source = new CancelAfterReadStream(File.ReadAllBytes(PathOf(SolidArchive)));
await using var archive = await SevenZipArchive.OpenAsyncArchive(
source,
ReaderOptions.ForExternalStream
);
var entries = await FileEntriesAsync(archive);
var stream = await entries[0].OpenEntryStreamAsync(CancellationToken.None);
var decoder = CachedFolderStream(archive);
source.CancelNextRead = true;
await using (stream.DisposeAsyncScope())
{
await Assert.ThrowsAsync<OperationCanceledException>(() =>
stream.CopyToAsync(Stream.Null, Constants.BufferSize, CancellationToken.None)
);
}
Assert.False(source.CancelNextRead);
Assert.Equal(ExpectedContent(SolidArchive, 1), await ReadAllAsync(entries[1]));
Assert.NotSame(decoder, CachedFolderStream(archive));
}
private sealed class CancelAfterReadStream(byte[] bytes) : MemoryStream(bytes)
{
internal bool CancelNextRead { get; set; }
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
var read = base.Read(buffer, offset, Math.Min(count, 64));
if (CancelNextRead)
{
CancelNextRead = false;
throw new OperationCanceledException();
}
return Task.FromResult(read);
}
#if !LEGACY_DOTNET
public override ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
var read = base.Read(buffer.Span.Slice(0, Math.Min(buffer.Length, 64)));
if (CancelNextRead)
{
CancelNextRead = false;
throw new OperationCanceledException();
}
return new ValueTask<int>(read);
}
#endif
}
[Fact]
public void FullyReadEntries_ReuseTheCachedFolderStream()
{
using var archive = SevenZipArchive.OpenArchive(PathOf(SolidArchive));
var entries = FileEntries(archive);
Assert.True(entries.Count >= 2);
ReadAll(entries[0]);
var decoder = CachedFolderStream(archive);
Assert.NotNull(decoder);
ReadAll(entries[1]);
// The decoder of a solid folder is shared by all of its entries and must not be recreated
// for every entry, otherwise extracting N entries decodes O(N^2) bytes.
Assert.Same(decoder, CachedFolderStream(archive));
}
[Fact]
public void PartialRead_ThenNextEntry_ReusesTheCachedFolderStream()
{
using var archive = SevenZipArchive.OpenArchive(PathOf(SolidArchive));
var entries = FileEntries(archive);
Assert.True(entries.Count >= 2);
ReadPartial(entries[0], 16);
var decoder = CachedFolderStream(archive);
Assert.NotNull(decoder);
ReadAll(entries[1]);
// The bytes that were not read are skipped forward, the folder is not decoded again.
Assert.Same(decoder, CachedFolderStream(archive));
}
[Fact]
public void Dispose_ReleasesTheCachedFolderStream_AndKeepsTheSourceStreamUsable()
{
using var source = File.OpenRead(PathOf(SolidArchive));
var archive = SevenZipArchive.OpenArchive(source, ReaderOptions.ForExternalStream);
var entries = FileEntries(archive);
ReadPartial(entries[0], 16);
var decoder = CachedFolderStream(archive);
Assert.NotNull(decoder);
archive.Dispose();
Assert.Null(CachedFolderStream(archive));
// The source stream is borrowed from the caller and must stay usable.
Assert.True(source.CanRead);
source.Position = 0;
Assert.True(source.ReadByte() >= 0);
}
}