mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-11-15 17:02:03 +00:00
Fix solid 7z cache positioning after partial entry reads
This commit is contained in:
@@ -914,6 +914,14 @@ For solid archives, prefer `ExtractAllEntries()` / `ExtractAllEntriesAsync()` to
|
||||
reuse the folder decoder across entries. A reader borrowed from an archive can
|
||||
be disposed without closing that archive's source.
|
||||
|
||||
The entries of a solid folder share one forward-only decoder. Reading only part
|
||||
of an entry does not corrupt the entries that follow: the decoder skips forward
|
||||
over the bytes that were not read when the next entry is opened. Read an entry
|
||||
before opening the next entry of the same folder - the shared decoder cannot
|
||||
serve two open entry streams of the same folder at the same time. Mixing
|
||||
synchronous and asynchronous entry reads of the same folder is supported; the
|
||||
decoder keeps the read mode it was first used with.
|
||||
|
||||
## 7z Encrypted Decoding Errors
|
||||
|
||||
AES-encrypted 7z codec data errors and encrypted-header CRC failures throw `SharpCompress.Common.CryptographicException`,
|
||||
|
||||
@@ -46,54 +46,53 @@ internal sealed partial class ArchiveDatabase
|
||||
CFolder folder,
|
||||
IPasswordProvider pw,
|
||||
long skipSize,
|
||||
long entrySize,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (_cachedFolder == folder && _cachedFolderStream != null)
|
||||
var tracker = await GetCachedFolderTrackerAsync(stream, folder, pw, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (skipSize < tracker.Position)
|
||||
{
|
||||
if (skipSize >= _cachedFolderStreamPosition)
|
||||
{
|
||||
var delta = skipSize - _cachedFolderStreamPosition;
|
||||
if (delta > 0)
|
||||
{
|
||||
await _cachedFolderStream
|
||||
.SkipAsync(delta, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
// Assume the caller will fully consume the returned entry stream, advancing the
|
||||
// shared stream by entrySize bytes.
|
||||
_cachedFolderStreamPosition = skipSize + entrySize;
|
||||
return _cachedFolderStream;
|
||||
}
|
||||
|
||||
// Non-sequential (backward) access within the same folder requires restarting.
|
||||
await DisposeCachedFolderStreamAsync().ConfigureAwait(false);
|
||||
tracker = await GetCachedFolderTrackerAsync(stream, folder, pw, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else if (_cachedFolderStream != null)
|
||||
|
||||
var delta = skipSize - tracker.Position;
|
||||
if (delta > 0)
|
||||
{
|
||||
await tracker.SkipAsync(delta, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return tracker;
|
||||
}
|
||||
|
||||
private async ValueTask<FolderStreamTracker> GetCachedFolderTrackerAsync(
|
||||
Stream stream,
|
||||
CFolder folder,
|
||||
IPasswordProvider pw,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (_cachedFolderStream is null || _cachedFolder != folder)
|
||||
{
|
||||
await DisposeCachedFolderStreamAsync().ConfigureAwait(false);
|
||||
_cachedFolderStream = await GetFolderStreamAsync(stream, folder, pw, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
_cachedFolderTracker = new FolderStreamTracker(_cachedFolderStream);
|
||||
_cachedFolder = folder;
|
||||
}
|
||||
|
||||
var newStream = await GetFolderStreamAsync(stream, folder, pw, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (skipSize > 0)
|
||||
{
|
||||
await newStream.SkipAsync(skipSize, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_cachedFolder = folder;
|
||||
_cachedFolderStream = newStream;
|
||||
_cachedFolderStreamPosition = skipSize + entrySize;
|
||||
return newStream;
|
||||
return _cachedFolderTracker.NotNull();
|
||||
}
|
||||
|
||||
internal async ValueTask DisposeCachedFolderStreamAsync()
|
||||
{
|
||||
var stream = _cachedFolderStream;
|
||||
_cachedFolderStream = null;
|
||||
_cachedFolderTracker = null;
|
||||
_cachedFolder = null;
|
||||
_cachedFolderStreamPosition = 0;
|
||||
if (stream is IAsyncDisposable asyncDisposable)
|
||||
{
|
||||
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -9,6 +9,11 @@ using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Common.SevenZip;
|
||||
|
||||
[SuppressMessage(
|
||||
"Design",
|
||||
"CA1001:Types that own disposable fields should be disposable",
|
||||
Justification = "The cached folder decoder is released by DisposeCachedFolderStream and DisposeCachedFolderStreamAsync, which the owning SevenZipArchive calls. The tracker only observes the decoder and does not own it."
|
||||
)]
|
||||
internal partial class ArchiveDatabase
|
||||
{
|
||||
internal byte _majorVersion;
|
||||
@@ -324,66 +329,67 @@ internal partial class ArchiveDatabase
|
||||
// file's stream was previously created fresh from the folder start and skipped forward.
|
||||
private CFolder? _cachedFolder;
|
||||
private Stream? _cachedFolderStream;
|
||||
private long _cachedFolderStreamPosition;
|
||||
private FolderStreamTracker? _cachedFolderTracker;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a stream positioned at <paramref name="skipSize"/> bytes into the decompressed
|
||||
/// contents of <paramref name="folder"/>. Reuses a cached decoder stream for the folder when
|
||||
/// possible instead of recreating and re-decoding from the start.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The returned stream is shared by every entry of the folder, so the caller must limit the
|
||||
/// reads to the entry it is interested in. The tracker records how much of the folder was
|
||||
/// really consumed, which is what allows the next entry to be positioned correctly even when
|
||||
/// the previous one was not read completely.
|
||||
/// </remarks>
|
||||
internal Stream GetFolderStream(
|
||||
Stream stream,
|
||||
CFolder folder,
|
||||
IPasswordProvider pw,
|
||||
long skipSize,
|
||||
long entrySize,
|
||||
bool enableParallelism = false
|
||||
)
|
||||
{
|
||||
if (_cachedFolder == folder && _cachedFolderStream != null)
|
||||
var tracker = GetCachedFolderTracker(stream, folder, pw, enableParallelism);
|
||||
if (skipSize < tracker.Position)
|
||||
{
|
||||
if (skipSize >= _cachedFolderStreamPosition)
|
||||
{
|
||||
var delta = skipSize - _cachedFolderStreamPosition;
|
||||
if (delta > 0)
|
||||
{
|
||||
_cachedFolderStream.Skip(delta);
|
||||
}
|
||||
// Assume the caller will fully consume the returned entry stream, advancing the
|
||||
// shared stream by entrySize bytes.
|
||||
_cachedFolderStreamPosition = skipSize + entrySize;
|
||||
return _cachedFolderStream;
|
||||
}
|
||||
|
||||
// Non-sequential (backward) access within the same folder requires restarting.
|
||||
_cachedFolderStream.Dispose();
|
||||
_cachedFolderStream = null;
|
||||
_cachedFolder = null;
|
||||
}
|
||||
else if (_cachedFolderStream != null)
|
||||
{
|
||||
_cachedFolderStream.Dispose();
|
||||
_cachedFolderStream = null;
|
||||
_cachedFolder = null;
|
||||
DisposeCachedFolderStream();
|
||||
tracker = GetCachedFolderTracker(stream, folder, pw, enableParallelism);
|
||||
}
|
||||
|
||||
var newStream = GetFolderStream(stream, folder, pw, enableParallelism);
|
||||
if (skipSize > 0)
|
||||
var delta = skipSize - tracker.Position;
|
||||
if (delta > 0)
|
||||
{
|
||||
newStream.Skip(skipSize);
|
||||
tracker.Skip(delta);
|
||||
}
|
||||
|
||||
_cachedFolder = folder;
|
||||
_cachedFolderStream = newStream;
|
||||
_cachedFolderStreamPosition = skipSize + entrySize;
|
||||
return newStream;
|
||||
return tracker;
|
||||
}
|
||||
|
||||
private FolderStreamTracker GetCachedFolderTracker(
|
||||
Stream stream,
|
||||
CFolder folder,
|
||||
IPasswordProvider pw,
|
||||
bool enableParallelism
|
||||
)
|
||||
{
|
||||
if (_cachedFolderStream is null || _cachedFolder != folder)
|
||||
{
|
||||
DisposeCachedFolderStream();
|
||||
_cachedFolderStream = GetFolderStream(stream, folder, pw, enableParallelism);
|
||||
_cachedFolderTracker = new FolderStreamTracker(_cachedFolderStream);
|
||||
_cachedFolder = folder;
|
||||
}
|
||||
|
||||
return _cachedFolderTracker.NotNull();
|
||||
}
|
||||
|
||||
internal void DisposeCachedFolderStream()
|
||||
{
|
||||
_cachedFolderStream?.Dispose();
|
||||
_cachedFolderStream = null;
|
||||
_cachedFolderTracker = null;
|
||||
_cachedFolder = null;
|
||||
_cachedFolderStreamPosition = 0;
|
||||
}
|
||||
}
|
||||
|
||||
169
src/SharpCompress/Common/SevenZip/FolderStreamTracker.cs
Normal file
169
src/SharpCompress/Common/SevenZip/FolderStreamTracker.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Common.SevenZip;
|
||||
|
||||
/// <summary>
|
||||
/// Keeps track of how much of a solid folder has actually been consumed from its shared decoder.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Every entry of a solid folder is decoded by the same forward-only stream, and that stream
|
||||
/// cannot be rewound. The position therefore has to be tracked explicitly: it tells the caller
|
||||
/// whether the next entry can be reached by skipping forward over the bytes that were not read
|
||||
/// (the common case) or whether the folder has to be decoded again from its start.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The tracker also pins the decoder to a single read mode. LzmaStream refuses to mix synchronous
|
||||
/// and asynchronous reads on the same instance, and the decoder is shared by every entry of the
|
||||
/// folder, so the first read decides the mode used by all later reads. Reads that arrive in the
|
||||
/// other mode are routed to the mode the decoder was established with instead of failing.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The tracker does not own the decoder: the ArchiveDatabase creates and disposes it. Disposing
|
||||
/// the tracker therefore leaves the shared decoder alone.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal sealed class FolderStreamTracker : Stream, IStreamStack
|
||||
{
|
||||
Stream IStreamStack.BaseStream() => _stream;
|
||||
|
||||
private readonly Stream _stream;
|
||||
private readonly byte[] _singleByte = new byte[1];
|
||||
private long _position;
|
||||
private bool _asyncMode;
|
||||
private bool _modeEstablished;
|
||||
|
||||
public FolderStreamTracker(Stream stream) =>
|
||||
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||
|
||||
/// <summary>
|
||||
/// The number of decompressed bytes consumed from the folder so far.
|
||||
/// </summary>
|
||||
public override long Position
|
||||
{
|
||||
get => _position;
|
||||
set
|
||||
{
|
||||
if (!_stream.CanSeek)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
_stream.Position = value;
|
||||
_position = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanRead => _stream.CanRead;
|
||||
|
||||
public override bool CanSeek => _stream.CanSeek;
|
||||
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override void Flush() { }
|
||||
|
||||
public override long Length => _stream.Length;
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (count <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (_modeEstablished && _asyncMode)
|
||||
{
|
||||
// The decoder is already used asynchronously, so it must not be read synchronously.
|
||||
#pragma warning disable VSTHRD002 // The decoder cannot be read synchronously in this mode.
|
||||
return ReadAsyncCore(buffer, offset, count, CancellationToken.None)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
#pragma warning restore VSTHRD002
|
||||
}
|
||||
|
||||
_modeEstablished = true;
|
||||
_asyncMode = false;
|
||||
var read = _stream.Read(buffer, offset, count);
|
||||
_position += read;
|
||||
return read;
|
||||
}
|
||||
|
||||
public override int ReadByte()
|
||||
{
|
||||
if (_modeEstablished && _asyncMode)
|
||||
{
|
||||
// The decoder is already used asynchronously, so it must not be read synchronously.
|
||||
#pragma warning disable VSTHRD002 // The decoder cannot be read synchronously in this mode.
|
||||
var read = ReadAsyncCore(_singleByte, 0, 1, CancellationToken.None)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
#pragma warning restore VSTHRD002
|
||||
return read == 0 ? -1 : _singleByte[0];
|
||||
}
|
||||
|
||||
_modeEstablished = true;
|
||||
_asyncMode = false;
|
||||
var value = _stream.ReadByte();
|
||||
if (value != -1)
|
||||
{
|
||||
_position++;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public override Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (count <= 0)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
if (_modeEstablished && !_asyncMode)
|
||||
{
|
||||
// The decoder is already used synchronously, so it must not be read asynchronously.
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return Task.FromResult(Read(buffer, offset, count));
|
||||
}
|
||||
|
||||
_modeEstablished = true;
|
||||
_asyncMode = true;
|
||||
return ReadAsyncCore(buffer, offset, count, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<int> ReadAsyncCore(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var read = await _stream
|
||||
.ReadAsync(buffer, offset, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
_position += read;
|
||||
return read;
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
var position = _stream.Seek(offset, origin);
|
||||
_position = position;
|
||||
return position;
|
||||
}
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public override void WriteByte(byte value) => throw new NotSupportedException();
|
||||
}
|
||||
@@ -61,7 +61,6 @@ internal class SevenZipFilePart : FilePart
|
||||
Folder!,
|
||||
_database.PasswordProvider,
|
||||
skipSize,
|
||||
Header.Size,
|
||||
_enableParallelism
|
||||
);
|
||||
return new ReadOnlySubStream(folderStream, Header.Size, leaveOpen: true);
|
||||
@@ -90,7 +89,6 @@ internal class SevenZipFilePart : FilePart
|
||||
Folder!,
|
||||
_database.PasswordProvider,
|
||||
skipSize,
|
||||
Header.Size,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
104
tests/SharpCompress.Test/SevenZip/FolderStreamTrackerTests.cs
Normal file
104
tests/SharpCompress.Test/SevenZip/FolderStreamTrackerTests.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.SevenZip;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.SevenZip;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the bookkeeping of the shared solid-folder decoder.
|
||||
/// </summary>
|
||||
public class FolderStreamTrackerTests
|
||||
{
|
||||
private static byte[] Payload(int size)
|
||||
{
|
||||
var data = new byte[size];
|
||||
for (var i = 0; i < size; i++)
|
||||
{
|
||||
data[i] = (byte)(i % 251);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reads_AdvanceTheTrackedPosition()
|
||||
{
|
||||
var payload = Payload(64);
|
||||
var tracker = new FolderStreamTracker(new MemoryStream(payload));
|
||||
Assert.Equal(0, tracker.Position);
|
||||
|
||||
var buffer = new byte[10];
|
||||
Assert.Equal(10, tracker.Read(buffer, 0, buffer.Length));
|
||||
Assert.Equal(10, tracker.Position);
|
||||
|
||||
Assert.Equal(payload[10], tracker.ReadByte());
|
||||
Assert.Equal(11, tracker.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AsyncReads_AdvanceTheTrackedPosition()
|
||||
{
|
||||
var tracker = new FolderStreamTracker(new MemoryStream(Payload(64)));
|
||||
var buffer = new byte[10];
|
||||
|
||||
Assert.Equal(10, await tracker.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None));
|
||||
Assert.Equal(10, tracker.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Skip_OverAForwardOnlyStream_ReadsTheSkippedBytes()
|
||||
{
|
||||
var payload = Payload(64);
|
||||
var tracker = new FolderStreamTracker(new ForwardOnlyStream(new MemoryStream(payload)));
|
||||
Assert.False(tracker.CanSeek);
|
||||
|
||||
tracker.Skip(16);
|
||||
Assert.Equal(16, tracker.Position);
|
||||
|
||||
var buffer = new byte[4];
|
||||
Assert.Equal(4, tracker.Read(buffer, 0, buffer.Length));
|
||||
Assert.Equal(payload.Skip(16).Take(4), buffer);
|
||||
Assert.Equal(20, tracker.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Skip_OverASeekableStream_SeeksAndKeepsThePosition()
|
||||
{
|
||||
// The parallel decode path hands out a seekable temp file instead of a decoder.
|
||||
var payload = Payload(64);
|
||||
var tracker = new FolderStreamTracker(new MemoryStream(payload));
|
||||
Assert.True(tracker.CanSeek);
|
||||
|
||||
tracker.Skip(16);
|
||||
Assert.Equal(16, tracker.Position);
|
||||
|
||||
var buffer = new byte[4];
|
||||
Assert.Equal(4, tracker.Read(buffer, 0, buffer.Length));
|
||||
Assert.Equal(payload.Skip(16).Take(4), buffer);
|
||||
Assert.Equal(20, tracker.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MixingReadModes_KeepsTheDecoderInASingleMode()
|
||||
{
|
||||
var payload = Payload(64);
|
||||
var tracker = new FolderStreamTracker(new MemoryStream(payload));
|
||||
var buffer = new byte[10];
|
||||
|
||||
Assert.Equal(10, tracker.Read(buffer, 0, buffer.Length));
|
||||
|
||||
// An asynchronous read after a synchronous one must not fail and must keep counting.
|
||||
Assert.Equal(10, await tracker.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None));
|
||||
Assert.Equal(20, tracker.Position);
|
||||
|
||||
// ... and the other way around.
|
||||
var other = new FolderStreamTracker(new MemoryStream(payload));
|
||||
Assert.Equal(10, await other.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None));
|
||||
Assert.Equal(10, other.Read(buffer, 0, buffer.Length));
|
||||
Assert.Equal(20, other.Position);
|
||||
}
|
||||
}
|
||||
345
tests/SharpCompress.Test/SevenZip/SevenZipSolidCacheTests.cs
Normal file
345
tests/SharpCompress.Test/SevenZip/SevenZipSolidCacheTests.cs
Normal file
@@ -0,0 +1,345 @@
|
||||
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()
|
||||
{
|
||||
var expected = ExpectedContent(SolidArchive, 2);
|
||||
|
||||
using var archive = SevenZipArchive.OpenArchive(PathOf(SolidArchive));
|
||||
var entries = FileEntries(archive);
|
||||
Assert.True(entries.Count >= 3);
|
||||
|
||||
ReadPartial(entries[0], 16);
|
||||
|
||||
Assert.Equal(expected, ReadAll(entries[2]));
|
||||
}
|
||||
|
||||
[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));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlternatingSyncAndAsyncEntryReads_ReturnEntryContent()
|
||||
{
|
||||
var expected = FileEntries(SevenZipArchive.OpenArchive(PathOf(SolidArchive)))
|
||||
.Select((_, index) => ExpectedContent(SolidArchive, index))
|
||||
.ToList();
|
||||
|
||||
using var archive = SevenZipArchive.OpenArchive(PathOf(SolidArchive));
|
||||
var entries = FileEntries(archive);
|
||||
Assert.True(entries.Count >= 3);
|
||||
|
||||
ReadPartial(entries[0], 16);
|
||||
|
||||
var second = entries[1].OpenEntryStream();
|
||||
using (second)
|
||||
{
|
||||
using var content = new MemoryStream();
|
||||
second.CopyTo(content);
|
||||
Assert.Equal(expected[1], content.ToArray());
|
||||
}
|
||||
|
||||
Assert.Equal(expected[2], ReadAll(entries[2]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AlternatingSyncAndAsyncEntryReads_ReturnEntryContentAsync()
|
||||
{
|
||||
var expected = new List<byte[]>();
|
||||
for (var index = 0; index < 3; index++)
|
||||
{
|
||||
expected.Add(await ExpectedContentAsync(SolidArchive, index).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
await using var archive = await SevenZipArchive
|
||||
.OpenAsyncArchive(PathOf(SolidArchive))
|
||||
.ConfigureAwait(false);
|
||||
var entries = await FileEntriesAsync(archive).ConfigureAwait(false);
|
||||
Assert.True(entries.Count >= 3);
|
||||
|
||||
await ReadPartialAsync(entries[0], 16).ConfigureAwait(false);
|
||||
|
||||
var second = entries[1].OpenEntryStream();
|
||||
using (second)
|
||||
{
|
||||
using var content = new MemoryStream();
|
||||
await second
|
||||
.CopyToAsync(content, Constants.BufferSize, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
Assert.Equal(expected[1], content.ToArray());
|
||||
}
|
||||
|
||||
Assert.Equal(expected[2], await ReadAllAsync(entries[2]).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user