mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-09-23 15:34:34 +00:00
Merge remote-tracking branch 'origin/release'
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.301",
|
||||
"version": "10.0.302",
|
||||
"rollForward": "disable"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,15 +71,12 @@ public static partial class IArchiveEntryExtensions
|
||||
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
|
||||
}
|
||||
|
||||
#if LEGACY_DOTNET
|
||||
using var entryStream = await archiveEntry
|
||||
var entryStream = await archiveEntry
|
||||
.OpenEntryStreamAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#else
|
||||
await using var entryStream = await archiveEntry
|
||||
.OpenEntryStreamAsync(cancellationToken)
|
||||
await using var entryStreamScope = entryStream
|
||||
.DisposeAsyncScope()
|
||||
.ConfigureAwait(false);
|
||||
#endif
|
||||
var checkedStream = options is null
|
||||
? entryStream
|
||||
: IEntryExtensions.WrapWithChecksumValidation(archiveEntry, entryStream, options);
|
||||
|
||||
@@ -18,7 +18,6 @@ public partial class EntryStream
|
||||
_completed = true;
|
||||
}
|
||||
|
||||
#if !LEGACY_DOTNET
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_isDisposed)
|
||||
@@ -44,9 +43,8 @@ public partial class EntryStream
|
||||
}
|
||||
}
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
await _stream.DisposeAsync().ConfigureAwait(false);
|
||||
await _stream.DisposeAsyncCompat().ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
[Zomp.SyncMethodGenerator.CreateSyncVersion]
|
||||
public override async Task<int> ReadAsync(
|
||||
|
||||
@@ -8,7 +8,7 @@ using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public partial class EntryStream : Stream
|
||||
public partial class EntryStream : AsyncDisposableStream
|
||||
{
|
||||
private readonly IReader _reader;
|
||||
private readonly Stream _stream;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
@@ -31,16 +30,16 @@ internal partial class RarCrcStream : RarStream
|
||||
.ConfigureAwait(false);
|
||||
if (result != 0)
|
||||
{
|
||||
currentCrc = RarCRC.CheckCrc(currentCrc, buffer, offset, result);
|
||||
_currentCrc = RarCRC.CheckCrc(_currentCrc, buffer, offset, result);
|
||||
}
|
||||
else if (
|
||||
!disableCRC
|
||||
&& GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0)
|
||||
!_disableCrc
|
||||
&& GetCrc() != BitConverter.ToUInt32(_readStream.NotNull().CurrentCrc.NotNull(), 0)
|
||||
&& count != 0
|
||||
)
|
||||
{
|
||||
// NOTE: we use the last FileHeader in a multipart volume to check CRC
|
||||
throw new InvalidFormatException("file crc mismatch");
|
||||
throw new InvalidFormatException("file crc mismatch: " + _key);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -56,16 +55,16 @@ internal partial class RarCrcStream : RarStream
|
||||
var result = await base.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
if (result != 0)
|
||||
{
|
||||
currentCrc = RarCRC.CheckCrc(currentCrc, buffer.Span, 0, result);
|
||||
_currentCrc = RarCRC.CheckCrc(_currentCrc, buffer.Span, 0, result);
|
||||
}
|
||||
else if (
|
||||
!disableCRC
|
||||
&& GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0)
|
||||
!_disableCrc
|
||||
&& GetCrc() != BitConverter.ToUInt32(_readStream.NotNull().CurrentCrc.NotNull(), 0)
|
||||
&& buffer.Length != 0
|
||||
)
|
||||
{
|
||||
// NOTE: we use the last FileHeader in a multipart volume to check CRC
|
||||
throw new InvalidFormatException("file crc mismatch");
|
||||
throw new InvalidFormatException("file crc mismatch: " + _key);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Rar.Headers;
|
||||
|
||||
@@ -9,9 +6,10 @@ namespace SharpCompress.Compressors.Rar;
|
||||
|
||||
internal partial class RarCrcStream : RarStream
|
||||
{
|
||||
private readonly MultiVolumeReadOnlyStreamBase readStream;
|
||||
private uint currentCrc;
|
||||
private readonly bool disableCRC;
|
||||
private readonly string? _key;
|
||||
private readonly MultiVolumeReadOnlyStreamBase _readStream;
|
||||
private uint _currentCrc;
|
||||
private readonly bool _disableCrc;
|
||||
|
||||
private RarCrcStream(
|
||||
IRarUnpack unpack,
|
||||
@@ -20,8 +18,9 @@ internal partial class RarCrcStream : RarStream
|
||||
)
|
||||
: base(unpack, fileHeader, readStream)
|
||||
{
|
||||
this.readStream = readStream;
|
||||
disableCRC = fileHeader.IsEncrypted;
|
||||
this._readStream = readStream;
|
||||
_key = fileHeader.FileName;
|
||||
_disableCrc = fileHeader.IsEncrypted;
|
||||
ResetCrc();
|
||||
}
|
||||
|
||||
@@ -36,31 +35,25 @@ internal partial class RarCrcStream : RarStream
|
||||
}
|
||||
|
||||
// Async methods moved to RarCrcStream.Async.cs
|
||||
public uint GetCrc() => ~_currentCrc;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
public uint GetCrc() => ~currentCrc;
|
||||
|
||||
public void ResetCrc() => currentCrc = 0xffffffff;
|
||||
public void ResetCrc() => _currentCrc = 0xffffffff;
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
var result = base.Read(buffer, offset, count);
|
||||
if (result != 0)
|
||||
{
|
||||
currentCrc = RarCRC.CheckCrc(currentCrc, buffer, offset, result);
|
||||
_currentCrc = RarCRC.CheckCrc(_currentCrc, buffer, offset, result);
|
||||
}
|
||||
else if (
|
||||
!disableCRC
|
||||
&& GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0)
|
||||
!_disableCrc
|
||||
&& GetCrc() != BitConverter.ToUInt32(_readStream.NotNull().CurrentCrc.NotNull(), 0)
|
||||
&& count != 0
|
||||
)
|
||||
{
|
||||
// NOTE: we use the last FileHeader in a multipart volume to check CRC
|
||||
throw new InvalidFormatException("file crc mismatch");
|
||||
throw new InvalidFormatException("file crc mismatch: " + _key);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -394,28 +394,34 @@ internal partial class Unpack
|
||||
{
|
||||
await UnpIO_UnpWriteAsync(OutMem, 0, BlockLength, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
WrittenFileSize += BlockLength;
|
||||
}
|
||||
|
||||
UnpSomeRead = true;
|
||||
WrittenFileSize += BlockLength;
|
||||
WrittenBorder = BlockEnd;
|
||||
WriteSizeLeft = (UnpPtr - WrittenBorder) & MaxWinMask;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
NotAllFiltersProcessed = true;
|
||||
// Current filter intersects the window write border, so we adjust
|
||||
// the window border to process this filter next time, not now.
|
||||
WrPtr = WrittenBorder;
|
||||
|
||||
// Since Filter start position can only increase, we quit processing
|
||||
// all following filters for this data block and reset 'NextWindow'
|
||||
// flag for them.
|
||||
for (var J = I; J < Filters.Count; J++)
|
||||
{
|
||||
var fltj = Filters[J];
|
||||
if (
|
||||
fltj.Type != FILTER_NONE
|
||||
&& fltj.NextWindow == false
|
||||
&& ((fltj.BlockStart - WrPtr) & MaxWinMask) < FullWriteSize
|
||||
)
|
||||
if (fltj.Type != FILTER_NONE)
|
||||
{
|
||||
fltj.NextWindow = true;
|
||||
fltj.NextWindow = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not write data left after current filter now.
|
||||
NotAllFiltersProcessed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,13 +151,16 @@ public class GZipFactory
|
||||
CompressionContext.FromStream(sharpCompressStream).WithReaderOptions(options)
|
||||
)
|
||||
);
|
||||
if (TarArchive.IsTarFile(testStream))
|
||||
var isTarArchive = TarArchive.IsTarFile(testStream);
|
||||
|
||||
// The TAR probe can consume arbitrary compressed input before it rejects a stream.
|
||||
sharpCompressStream.Rewind();
|
||||
sharpCompressStream.StopRecording();
|
||||
if (isTarArchive)
|
||||
{
|
||||
sharpCompressStream.StopRecording();
|
||||
reader = new TarReader(sharpCompressStream, options, CompressionType.GZip);
|
||||
return true;
|
||||
}
|
||||
sharpCompressStream.StopRecording();
|
||||
reader = OpenReader(sharpCompressStream, options);
|
||||
return true;
|
||||
}
|
||||
@@ -182,15 +185,27 @@ public class GZipFactory
|
||||
}
|
||||
|
||||
sharpCompressStream.Rewind();
|
||||
var tarReader = await new TarFactory()
|
||||
.TryOpenReaderAsync(sharpCompressStream, options, cancellationToken)
|
||||
using var testStream = SharpCompressStream.CreateNonDisposing(
|
||||
await options
|
||||
.Providers.CreateDecompressStreamAsync(
|
||||
CompressionType.GZip,
|
||||
SharpCompressStream.CreateNonDisposing(sharpCompressStream),
|
||||
CompressionContext.FromStream(sharpCompressStream).WithReaderOptions(options),
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false)
|
||||
);
|
||||
var isTarArchive = await TarArchive
|
||||
.IsTarFileAsync(testStream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (tarReader is not null)
|
||||
|
||||
sharpCompressStream.Rewind();
|
||||
sharpCompressStream.StopRecording();
|
||||
if (isTarArchive)
|
||||
{
|
||||
return tarReader;
|
||||
return new TarReader(sharpCompressStream, options, CompressionType.GZip);
|
||||
}
|
||||
|
||||
sharpCompressStream.StopRecording();
|
||||
return await OpenAsyncReader(sharpCompressStream, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -65,14 +65,17 @@ public class LzwFactory : Factory, IReaderFactory
|
||||
)
|
||||
)
|
||||
{
|
||||
if (TarArchive.IsTarFile(testStream))
|
||||
var isTarArchive = TarArchive.IsTarFile(testStream);
|
||||
|
||||
// The TAR probe can consume arbitrary compressed input before it rejects a stream.
|
||||
sharpCompressStream.Rewind();
|
||||
sharpCompressStream.StopRecording();
|
||||
if (isTarArchive)
|
||||
{
|
||||
sharpCompressStream.StopRecording();
|
||||
reader = new TarReader(sharpCompressStream, options, CompressionType.Lzw);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
sharpCompressStream.StopRecording();
|
||||
reader = OpenReader(sharpCompressStream, options);
|
||||
return true;
|
||||
}
|
||||
@@ -97,15 +100,26 @@ public class LzwFactory : Factory, IReaderFactory
|
||||
}
|
||||
|
||||
sharpCompressStream.Rewind();
|
||||
var tarReader = await new TarFactory()
|
||||
.TryOpenReaderAsync(sharpCompressStream, options, cancellationToken)
|
||||
using var testStream = SharpCompressStream.CreateNonDisposing(
|
||||
await options
|
||||
.Providers.CreateDecompressStreamAsync(
|
||||
CompressionType.Lzw,
|
||||
SharpCompressStream.CreateNonDisposing(sharpCompressStream),
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false)
|
||||
);
|
||||
var isTarArchive = await TarArchive
|
||||
.IsTarFileAsync(testStream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (tarReader is not null)
|
||||
|
||||
sharpCompressStream.Rewind();
|
||||
sharpCompressStream.StopRecording();
|
||||
if (isTarArchive)
|
||||
{
|
||||
return tarReader;
|
||||
return new TarReader(sharpCompressStream, options, CompressionType.Lzw);
|
||||
}
|
||||
|
||||
sharpCompressStream.StopRecording();
|
||||
return await OpenAsyncReader(sharpCompressStream, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
36
src/SharpCompress/IO/AsyncDisposableStream.cs
Normal file
36
src/SharpCompress/IO/AsyncDisposableStream.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.IO;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="Stream"/> that is guaranteed to be asynchronously disposable on every target framework.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// On .NET Framework 4.8 and .NET Standard 2.0, <see cref="Stream"/> has no <c>DisposeAsync</c>.
|
||||
/// <c>Microsoft.Bcl.AsyncInterfaces</c> supplies the <see cref="IAsyncDisposable"/> interface on those
|
||||
/// targets but cannot retrofit it onto the BCL's <see cref="Stream"/>, and C# will not accept an
|
||||
/// extension method for the pattern - <c>await using</c> requires a reachable <em>instance</em>
|
||||
/// <c>DisposeAsync</c>. Deriving from this class instead of <see cref="Stream"/> therefore makes a type
|
||||
/// usable with <c>await using</c> uniformly, with no conditional compilation at the call site.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The fallback below is the same behaviour as the BCL's own default <see cref="Stream.DisposeAsync"/>,
|
||||
/// so a derived type may call <c>await base.DisposeAsync()</c> unconditionally on any target.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class AsyncDisposableStream : Stream
|
||||
#if NO_STREAM_DISPOSEASYNC
|
||||
, IAsyncDisposable
|
||||
#endif
|
||||
{
|
||||
#if NO_STREAM_DISPOSEASYNC
|
||||
public virtual ValueTask DisposeAsync()
|
||||
{
|
||||
Dispose();
|
||||
return default;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
36
src/SharpCompress/IO/AsyncDisposeScope.cs
Normal file
36
src/SharpCompress/IO/AsyncDisposeScope.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Makes any resource usable with <c>await using</c>, disposing it asynchronously when the runtime type
|
||||
/// supports it and synchronously otherwise.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Needed for locals whose <em>static</em> type is <see cref="System.IO.Stream"/> (or another type that
|
||||
/// only sometimes has <c>DisposeAsync</c>), where <c>await using</c> cannot bind directly on
|
||||
/// .NET Framework 4.8 / .NET Standard 2.0. Unlike a compile-time guard, this picks the asynchronous path
|
||||
/// based on the runtime type, so a stream that really is asynchronously disposable is disposed that way on
|
||||
/// every target framework. Prefer deriving from <see cref="AsyncDisposableStream"/> where the type is ours.
|
||||
/// </remarks>
|
||||
internal readonly struct AsyncDisposeScope(IDisposable? resource) : IAsyncDisposable
|
||||
{
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
if (resource is IAsyncDisposable asyncDisposable)
|
||||
{
|
||||
return asyncDisposable.DisposeAsync();
|
||||
}
|
||||
|
||||
resource?.Dispose();
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors <c>ConfiguredAsyncDisposable</c> so <c>await using</c> can specify context capture without
|
||||
/// boxing this struct through <see cref="IAsyncDisposable"/>.
|
||||
/// </summary>
|
||||
public ConfiguredAsyncDisposeScope ConfigureAwait(bool continueOnCapturedContext) =>
|
||||
new(resource, continueOnCapturedContext);
|
||||
}
|
||||
22
src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs
Normal file
22
src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.IO;
|
||||
|
||||
internal readonly struct ConfiguredAsyncDisposeScope(
|
||||
IDisposable? resource,
|
||||
bool continueOnCapturedContext
|
||||
)
|
||||
{
|
||||
public ConfiguredValueTaskAwaitable DisposeAsync()
|
||||
{
|
||||
if (resource is IAsyncDisposable asyncDisposable)
|
||||
{
|
||||
return asyncDisposable.DisposeAsync().ConfigureAwait(continueOnCapturedContext);
|
||||
}
|
||||
|
||||
resource?.Dispose();
|
||||
return default(ValueTask).ConfigureAwait(continueOnCapturedContext);
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,16 @@ internal sealed partial class SeekableSharpCompressStream : SharpCompressStream
|
||||
public override void StartRecording(int? minBufferSize = null) =>
|
||||
_recordedPosition = _stream.Position;
|
||||
|
||||
public override void StopRecording() => _recordedPosition = null;
|
||||
public override void StopRecording()
|
||||
{
|
||||
if (_recordedPosition.HasValue)
|
||||
{
|
||||
// Seek back to the recording anchor position, matching the behavior of the
|
||||
// non-seekable SharpCompressStream.StopRecording() which rewinds _logicalPosition.
|
||||
_stream.Seek(_recordedPosition.Value, SeekOrigin.Begin);
|
||||
}
|
||||
_recordedPosition = null;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
|
||||
@@ -25,6 +25,20 @@ public static class StreamExtensions
|
||||
|
||||
public void Skip() => stream.CopyTo(Stream.Null);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a scope that disposes this stream when awaited, asynchronously where the runtime type
|
||||
/// supports it. Lets <c>await using</c> be written against a <see cref="Stream"/>-typed local on
|
||||
/// every target framework.
|
||||
/// </summary>
|
||||
internal AsyncDisposeScope DisposeAsyncScope() => new(stream);
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this stream, asynchronously where the runtime type supports it. Use where the static
|
||||
/// type is <see cref="Stream"/>, which has no <c>DisposeAsync</c> on .NET Framework 4.8 /
|
||||
/// .NET Standard 2.0.
|
||||
/// </summary>
|
||||
internal ValueTask DisposeAsyncCompat() => new AsyncDisposeScope(stream).DisposeAsync();
|
||||
|
||||
public async ValueTask SkipAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -104,13 +104,8 @@ public abstract partial class AbstractReader<TEntry, TVolume>
|
||||
}
|
||||
}
|
||||
//don't know the size so we have to try to decompress to skip
|
||||
#if LEGACY_DOTNET
|
||||
using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
public async ValueTask WriteEntryToAsync(
|
||||
@@ -139,19 +134,11 @@ public abstract partial class AbstractReader<TEntry, TVolume>
|
||||
|
||||
private async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken)
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var sourceStream = WrapWithProgress(s, Entry);
|
||||
await sourceStream
|
||||
.CopyToAsync(writeStream, Options.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#else
|
||||
await using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var sourceStream = WrapWithProgress(s, Entry);
|
||||
await sourceStream
|
||||
.CopyToAsync(writeStream, Options.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
public async ValueTask<EntryStream> OpenEntryStreamAsync(
|
||||
|
||||
@@ -108,15 +108,9 @@ public static class IAsyncReaderExtensions
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
using var entryStream = await reader
|
||||
.OpenEntryStreamAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#else
|
||||
await using var entryStream = await reader
|
||||
.OpenEntryStreamAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#endif
|
||||
var checkedStream = IEntryExtensions.WrapWithChecksumValidation(
|
||||
reader.Entry,
|
||||
entryStream,
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'net48' Or '$(TargetFramework)' == 'netstandard2.0' Or '$(TargetFramework)' == 'netstandard2.1' ">
|
||||
<DefineConstants>$(DefineConstants);LEGACY_DOTNET</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'net48' Or '$(TargetFramework)' == 'netstandard2.0'">
|
||||
<DefineConstants>$(DefineConstants);NO_STREAM_DISPOSEASYNC</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(TargetFramework)' == 'net8.0' Or '$(TargetFramework)' == 'net10.0' ">
|
||||
<IsTrimmable>true</IsTrimmable>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
|
||||
@@ -339,9 +339,9 @@
|
||||
"net10.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.9, )",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "4Iw41e2h7I4t70SJcX2GCmbyKJIlA273Cfm9RJMM050/3VBejGAG1KcthP5Z2L6SQcbfbf6BhNWO26+ZG+GzMg=="
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
@@ -471,9 +471,9 @@
|
||||
"net8.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.28, )",
|
||||
"resolved": "8.0.28",
|
||||
"contentHash": "XMqgVjlLxLqWmEh3c49haXLQwsMNtvo6YscUaqfvEGfg1iA8hnYgkUVq3i9Zu9gKeNKMWiiZKVwZExc/qyEAsQ=="
|
||||
"requested": "[8.0.29, )",
|
||||
"resolved": "8.0.29",
|
||||
"contentHash": "HSBTfrkIZijz8z3ybLRKB7E8rHk4QQufFwpHa9fc5CMIgRhRzdn4mBGmlyXZqaueiMPtuJcnjresGvSTfaW8Mg=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
"net10.0": {
|
||||
"Microsoft.DotNet.ILCompiler": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.9, )",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "4y+VsQOcs4EiTSINdCpCWi/aLRbIbGTxSezQXd8uGVhzbDRm1FNVTZDyCUQixE0+g9UFusvfxVcF68YYz7RzxA=="
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "tnG8ntt/Bk6odvHREnGLMo3PEiihy5iSlIFVp0JbIo00GKtNRt2k73eKZbPqR5yaJNIa3z8R86YLwbxfqpb17g=="
|
||||
},
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.9, )",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "4Iw41e2h7I4t70SJcX2GCmbyKJIlA273Cfm9RJMM050/3VBejGAG1KcthP5Z2L6SQcbfbf6BhNWO26+ZG+GzMg=="
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "f5VCIE7AJpd5YvzNTeMGVzQIgyE9tX+AreTYwQF+REbu+DZo/2Ae+jNSwhPEYrVz6RRkd7y8ubXjk6Nn6Ka+Cg=="
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"type": "Direct",
|
||||
@@ -82,17 +82,17 @@
|
||||
"net10.0/linux-x64": {
|
||||
"Microsoft.DotNet.ILCompiler": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.9, )",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "4y+VsQOcs4EiTSINdCpCWi/aLRbIbGTxSezQXd8uGVhzbDRm1FNVTZDyCUQixE0+g9UFusvfxVcF68YYz7RzxA==",
|
||||
"requested": "[10.0.10, )",
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "tnG8ntt/Bk6odvHREnGLMo3PEiihy5iSlIFVp0JbIo00GKtNRt2k73eKZbPqR5yaJNIa3z8R86YLwbxfqpb17g==",
|
||||
"dependencies": {
|
||||
"runtime.linux-x64.Microsoft.DotNet.ILCompiler": "10.0.9"
|
||||
"runtime.linux-x64.Microsoft.DotNet.ILCompiler": "10.0.10"
|
||||
}
|
||||
},
|
||||
"runtime.linux-x64.Microsoft.DotNet.ILCompiler": {
|
||||
"type": "Transitive",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "45CVefG8S0eUKUJ4LBWOi8FOAgMJOP6exW9l5M9OjvQaGR7jvkokBK50XaZCsO66uLxABcuzvncV8A3YiJLUgw=="
|
||||
"resolved": "10.0.10",
|
||||
"contentHash": "WRjSRBfv6A6UjgjO8EQuLe9xqdICpkQx1hACUziCw4B2uGL+2jVhkFLq/G7rxRr3MGvqLo9B+nNdfIJ/5CYN7A=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,19 +151,8 @@ public class BZip2StreamAsyncTests
|
||||
Assert.True(compressed.Length > 0);
|
||||
|
||||
// Decompress and verify
|
||||
#if LEGACY_DOTNET
|
||||
// MemoryStream has nothing to dispose asynchronously
|
||||
using (var readStream = new MemoryStream(compressed))
|
||||
{
|
||||
using (
|
||||
var bzip2Stream = await BZip2Stream.CreateAsync(
|
||||
new AsyncOnlyStream(readStream),
|
||||
SharpCompress.Compressors.CompressionMode.Decompress,
|
||||
false
|
||||
)
|
||||
)
|
||||
{
|
||||
#else
|
||||
await using (var readStream = new MemoryStream(compressed))
|
||||
{
|
||||
await using (
|
||||
var bzip2Stream = await BZip2Stream.CreateAsync(
|
||||
@@ -173,7 +162,6 @@ public class BZip2StreamAsyncTests
|
||||
)
|
||||
)
|
||||
{
|
||||
#endif
|
||||
var result = new StringBuilder();
|
||||
var buffer = new byte[256];
|
||||
int bytesRead;
|
||||
|
||||
@@ -68,11 +68,8 @@ public class GZipCrcExtractionTests : TestBase
|
||||
[Fact]
|
||||
public async Task GZipArchive_WriteToFileAsync_Throws_On_Crc_Mismatch()
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
// MemoryStream has nothing to dispose asynchronously
|
||||
using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: true));
|
||||
#else
|
||||
await using var stream = new MemoryStream(ReadCorruptedGZipTrailer(corruptCrc: true));
|
||||
#endif
|
||||
await using var archive = await GZipArchive.OpenAsyncArchive(stream);
|
||||
var entry = await archive.EntriesAsync.SingleAsync();
|
||||
var destination = Path.Combine(SCRATCH_FILES_PATH, Guid.NewGuid().ToString());
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
@@ -37,4 +38,33 @@ public class GZipReaderAsyncTests : ReaderTests
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async ValueTask GZip_ReaderFactory_FlatGZip_Async()
|
||||
{
|
||||
var source = new byte[2048];
|
||||
for (var i = 0; i < source.Length; i++)
|
||||
{
|
||||
source[i] = 0xFF;
|
||||
}
|
||||
|
||||
var gzipPath = Path.Combine(SCRATCH_FILES_PATH, "Flat.bin.gz");
|
||||
using (var output = File.Create(gzipPath))
|
||||
using (var gzip = new GZipStream(output, CompressionMode.Compress))
|
||||
{
|
||||
await gzip.WriteAsync(source, 0, source.Length);
|
||||
}
|
||||
|
||||
using Stream stream = File.OpenRead(gzipPath);
|
||||
await using var reader = await ReaderFactory.OpenAsyncReader(stream);
|
||||
Assert.IsType<GZipReader>(reader);
|
||||
Assert.True(await reader.MoveToNextEntryAsync());
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
await reader.WriteEntryToAsync(ms);
|
||||
Assert.Equal(source.Length, ms.Length);
|
||||
Assert.Equal(source, ms.ToArray());
|
||||
|
||||
Assert.False(await reader.MoveToNextEntryAsync());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,12 @@ public class GZipReaderTests : ReaderTests
|
||||
using var reader = ReaderFactory.OpenReader(stream);
|
||||
Assert.IsType<GZipReader>(reader);
|
||||
Assert.True(reader.MoveToNextEntry());
|
||||
|
||||
using var ms = new MemoryStream();
|
||||
reader.WriteEntryTo(ms);
|
||||
Assert.Equal(source.Length, ms.Length);
|
||||
Assert.Equal(source, ms.ToArray());
|
||||
|
||||
Assert.False(reader.MoveToNextEntry());
|
||||
}
|
||||
}
|
||||
|
||||
208
tests/SharpCompress.Test/LargeArchiveTests.cs
Normal file
208
tests/SharpCompress.Test/LargeArchiveTests.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Crypto;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test;
|
||||
|
||||
[Collection(LargeArchiveCollection.Name)]
|
||||
public class LargeArchiveTests : TestBase
|
||||
{
|
||||
private const long LargeFileSize = 64L * 1024 * 1024;
|
||||
private const uint LargeFileCrc = 0xF9081EB0;
|
||||
private const int BufferSize = 64 * 1024;
|
||||
|
||||
[Theory]
|
||||
[InlineData("Large/Large.zip")]
|
||||
[InlineData("Large/Large.tar")]
|
||||
[InlineData("Large/Large.gz")]
|
||||
[InlineData("Large/Large.rar")]
|
||||
[InlineData("Large/Large.7z")]
|
||||
public void OpenArchive_ShouldStreamLargeEntry(string fixtureName)
|
||||
{
|
||||
using var stream = File.OpenRead(GetMaterializedFixturePath(fixtureName));
|
||||
using var archive = ArchiveFactory.OpenArchive(stream);
|
||||
|
||||
VerifyArchive(archive);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Large/Large.zip")]
|
||||
[InlineData("Large/Large.tar")]
|
||||
[InlineData("Large/Large.gz")]
|
||||
[InlineData("Large/Large.rar")]
|
||||
[InlineData("Large/Large.7z")]
|
||||
public async Task OpenAsyncArchive_ShouldStreamLargeEntry(string fixtureName)
|
||||
{
|
||||
await using var stream = new AsyncOnlyStream(
|
||||
File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName))
|
||||
);
|
||||
await using var archive = await ArchiveFactory.OpenAsyncArchive(stream);
|
||||
|
||||
var entry = await GetSingleEntryAsync(archive);
|
||||
|
||||
var entryStream = await entry.OpenEntryStreamAsync();
|
||||
await using var entryStreamScope = entryStream.DisposeAsyncScope();
|
||||
await VerifyContentAsync(entry.Key, entryStream);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Large/Large.zip")]
|
||||
[InlineData("Large/Large.tar")]
|
||||
[InlineData("Large/Large.gz")]
|
||||
[InlineData("Large/Large.rar")]
|
||||
[InlineData("Large/Large.tar.gz")]
|
||||
public void OpenReader_ShouldStreamLargeEntry(string fixtureName)
|
||||
{
|
||||
using var stream = File.OpenRead(GetMaterializedFixturePath(fixtureName));
|
||||
using var reader = ReaderFactory.OpenReader(stream);
|
||||
|
||||
VerifyReader(reader);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Large/Large.zip")]
|
||||
[InlineData("Large/Large.tar")]
|
||||
[InlineData("Large/Large.gz")]
|
||||
[InlineData("Large/Large.rar")]
|
||||
[InlineData("Large/Large.tar.gz")]
|
||||
public async Task OpenAsyncReader_ShouldStreamLargeEntry(string fixtureName)
|
||||
{
|
||||
await using var stream = new AsyncOnlyStream(
|
||||
File.OpenRead(await GetMaterializedFixturePathAsync(fixtureName))
|
||||
);
|
||||
await using var reader = await ReaderFactory.OpenAsyncReader(stream);
|
||||
|
||||
Assert.True(await reader.MoveToNextEntryAsync());
|
||||
Assert.False(reader.Entry.IsDirectory);
|
||||
|
||||
await using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
await VerifyContentAsync(reader.Entry.Key, entryStream);
|
||||
Assert.False(await reader.MoveToNextEntryAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenAsyncReader_WithGZipExtensionHint_ShouldStreamLargeTarEntry()
|
||||
{
|
||||
using var file = File.OpenRead(GetFixturePath("Large/Large.tar.gz"));
|
||||
await using var stream = new AsyncOnlyStream(new ForwardOnlyStream(file));
|
||||
var options = new ReaderOptions { ExtensionHint = "gz" };
|
||||
|
||||
await using var reader = await ReaderFactory.OpenAsyncReader(stream, options);
|
||||
|
||||
Assert.True(await reader.MoveToNextEntryAsync());
|
||||
Assert.False(reader.Entry.IsDirectory);
|
||||
await using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
await VerifyContentAsync(reader.Entry.Key, entryStream);
|
||||
Assert.False(await reader.MoveToNextEntryAsync());
|
||||
}
|
||||
|
||||
private static string GetFixturePath(string fixtureName) =>
|
||||
Path.Combine(TEST_ARCHIVES_PATH, fixtureName);
|
||||
|
||||
private string GetMaterializedFixturePath(string fixtureName) =>
|
||||
fixtureName == "Large/Large.tar" ? MaterializeTarFixture() : GetFixturePath(fixtureName);
|
||||
|
||||
private async Task<string> GetMaterializedFixturePathAsync(string fixtureName) =>
|
||||
fixtureName == "Large/Large.tar"
|
||||
? await MaterializeTarFixtureAsync()
|
||||
: GetFixturePath(fixtureName);
|
||||
|
||||
private string MaterializeTarFixture()
|
||||
{
|
||||
var tarPath = Path.Combine(SCRATCH_FILES_PATH, "Large.tar");
|
||||
using var compressedStream = File.OpenRead(GetFixturePath("Large/Large.tar.gz"));
|
||||
using var gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress);
|
||||
using var tarStream = File.Create(tarPath);
|
||||
gzipStream.CopyTo(tarStream);
|
||||
return tarPath;
|
||||
}
|
||||
|
||||
private async Task<string> MaterializeTarFixtureAsync()
|
||||
{
|
||||
var tarPath = Path.Combine(SCRATCH_FILES_PATH, "Large.tar");
|
||||
using var compressedStream = File.OpenRead(GetFixturePath("Large/Large.tar.gz"));
|
||||
using var gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress);
|
||||
using var tarStream = File.Create(tarPath);
|
||||
await gzipStream.CopyToAsync(tarStream);
|
||||
return tarPath;
|
||||
}
|
||||
|
||||
private static void VerifyArchive(IArchive archive)
|
||||
{
|
||||
var entry = Assert.Single(archive.Entries);
|
||||
|
||||
Assert.False(entry.IsDirectory);
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
VerifyContent(entry.Key, entryStream);
|
||||
}
|
||||
|
||||
private static async Task<IArchiveEntry> GetSingleEntryAsync(IAsyncArchive archive)
|
||||
{
|
||||
IArchiveEntry? entry = null;
|
||||
await foreach (var candidate in archive.EntriesAsync)
|
||||
{
|
||||
Assert.Null(entry);
|
||||
entry = candidate;
|
||||
}
|
||||
|
||||
return entry ?? throw new InvalidOperationException("The archive contains no entries.");
|
||||
}
|
||||
|
||||
private static void VerifyReader(IReader reader)
|
||||
{
|
||||
Assert.True(reader.MoveToNextEntry());
|
||||
Assert.False(reader.Entry.IsDirectory);
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
VerifyContent(reader.Entry.Key, entryStream);
|
||||
Assert.False(reader.MoveToNextEntry());
|
||||
}
|
||||
|
||||
private static void VerifyContent(string? key, Stream entryStream)
|
||||
{
|
||||
Assert.Equal("large.bin", key);
|
||||
|
||||
using var crcStream = new Crc32Stream(Stream.Null);
|
||||
var buffer = new byte[BufferSize];
|
||||
long length = 0;
|
||||
int bytesRead;
|
||||
while ((bytesRead = entryStream.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
crcStream.Write(buffer, 0, bytesRead);
|
||||
length += bytesRead;
|
||||
}
|
||||
|
||||
Assert.Equal(LargeFileSize, length);
|
||||
Assert.Equal(LargeFileCrc, crcStream.Crc);
|
||||
}
|
||||
|
||||
private static async Task VerifyContentAsync(string? key, Stream entryStream)
|
||||
{
|
||||
Assert.Equal("large.bin", key);
|
||||
|
||||
using var crcStream = new Crc32Stream(Stream.Null);
|
||||
var buffer = new byte[BufferSize];
|
||||
long length = 0;
|
||||
int bytesRead;
|
||||
while ((bytesRead = await entryStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
await crcStream.WriteAsync(buffer, 0, bytesRead);
|
||||
length += bytesRead;
|
||||
}
|
||||
|
||||
Assert.Equal(LargeFileSize, length);
|
||||
Assert.Equal(LargeFileCrc, crcStream.Crc);
|
||||
}
|
||||
}
|
||||
|
||||
[CollectionDefinition(LargeArchiveCollection.Name, DisableParallelization = true)]
|
||||
public sealed class LargeArchiveCollection
|
||||
{
|
||||
public const string Name = "Large archive fixtures";
|
||||
}
|
||||
@@ -2,10 +2,11 @@ using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Test.Mocks;
|
||||
|
||||
public class AsyncOnlyStream(Stream stream, bool disposeStream = true) : Stream
|
||||
public class AsyncOnlyStream(Stream stream, bool disposeStream = true) : AsyncDisposableStream
|
||||
{
|
||||
private readonly Stream _stream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Test.Mocks;
|
||||
|
||||
@@ -8,7 +9,7 @@ namespace SharpCompress.Test.Mocks;
|
||||
// CryptoStream doesn't always trigger the Flush, so this class is used instead
|
||||
// See https://referencesource.microsoft.com/#mscorlib/system/security/cryptography/cryptostream.cs,141
|
||||
|
||||
public class FlushOnDisposeStream(Stream innerStream) : Stream
|
||||
public class FlushOnDisposeStream(Stream innerStream) : AsyncDisposableStream
|
||||
{
|
||||
public override bool CanRead => innerStream.CanRead;
|
||||
|
||||
@@ -48,12 +49,10 @@ public class FlushOnDisposeStream(Stream innerStream) : Stream
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#if !LEGACY_DOTNET
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await innerStream.FlushAsync();
|
||||
innerStream.Close();
|
||||
await base.DisposeAsync();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Test.Mocks;
|
||||
|
||||
@@ -9,7 +10,7 @@ namespace SharpCompress.Test.Mocks;
|
||||
/// A forward-only stream wrapper that delegates directly to the underlying stream
|
||||
/// without any buffering. Supports reading and writing but not seeking.
|
||||
/// </summary>
|
||||
public class ForwardOnlyStream : Stream
|
||||
public class ForwardOnlyStream : AsyncDisposableStream
|
||||
{
|
||||
private readonly Stream _stream;
|
||||
private bool _isDisposed;
|
||||
@@ -142,17 +143,15 @@ public class ForwardOnlyStream : Stream
|
||||
}
|
||||
}
|
||||
|
||||
#if !LEGACY_DOTNET
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
await _stream.DisposeAsync();
|
||||
await _stream.DisposeAsyncCompat();
|
||||
_isDisposed = true;
|
||||
}
|
||||
await base.DisposeAsync();
|
||||
}
|
||||
#endif
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Test.Mocks;
|
||||
|
||||
public class TestStream(Stream stream, bool read, bool write, bool seek) : Stream
|
||||
public class TestStream(Stream stream, bool read, bool write, bool seek) : AsyncDisposableStream
|
||||
{
|
||||
public TestStream(Stream stream)
|
||||
: this(stream, stream.CanRead, stream.CanWrite, stream.CanSeek) { }
|
||||
@@ -50,14 +51,14 @@ public class TestStream(Stream stream, bool read, bool write, bool seek) : Strea
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
) => stream.ReadAsync(buffer, cancellationToken);
|
||||
#endif
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await base.DisposeAsync();
|
||||
await stream.DisposeAsync();
|
||||
await stream.DisposeAsyncCompat();
|
||||
IsDisposed = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Test.Mocks;
|
||||
|
||||
@@ -9,7 +10,7 @@ namespace SharpCompress.Test.Mocks;
|
||||
/// A stream wrapper that throws NotSupportedException on Flush() calls.
|
||||
/// This is used to test that archive iteration handles streams that don't support flushing.
|
||||
/// </summary>
|
||||
public class ThrowOnFlushStream : Stream
|
||||
public class ThrowOnFlushStream : AsyncDisposableStream
|
||||
{
|
||||
private readonly Stream inner;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Test.Mocks;
|
||||
|
||||
@@ -7,7 +8,7 @@ namespace SharpCompress.Test.Mocks;
|
||||
/// A stream wrapper that truncates the underlying stream after reading a specified number of bytes.
|
||||
/// Used for testing error handling when streams end prematurely.
|
||||
/// </summary>
|
||||
public class TruncatedStream : Stream
|
||||
public class TruncatedStream : AsyncDisposableStream
|
||||
{
|
||||
private readonly Stream baseStream;
|
||||
private readonly long truncateAfterBytes;
|
||||
|
||||
@@ -159,18 +159,12 @@ public abstract class ReaderTests : TestBase
|
||||
{
|
||||
using var file = File.OpenRead(testArchive);
|
||||
|
||||
#if !LEGACY_DOTNET
|
||||
await using var protectedStream = SharpCompressStream.CreateNonDisposing(
|
||||
// SharpCompressStream is not yet an AsyncDisposableStream, so scope its disposal
|
||||
var protectedStream = SharpCompressStream.CreateNonDisposing(
|
||||
new ForwardOnlyStream(file, options.BufferSize)
|
||||
);
|
||||
await using var protectedStreamScope = protectedStream.DisposeAsyncScope();
|
||||
await using var testStream = new TestStream(protectedStream);
|
||||
#else
|
||||
|
||||
using var protectedStream = SharpCompressStream.CreateNonDisposing(
|
||||
new ForwardOnlyStream(file, options.BufferSize)
|
||||
);
|
||||
using var testStream = new TestStream(protectedStream);
|
||||
#endif
|
||||
await using (
|
||||
var reader = await ReaderFactory.OpenAsyncReader(
|
||||
new AsyncOnlyStream(testStream),
|
||||
|
||||
@@ -187,10 +187,6 @@ public class TarArchiveAsyncTests : ArchiveTests
|
||||
}
|
||||
}
|
||||
}
|
||||
#if LEGACY_DOTNET
|
||||
//add a delay because old .net sucks on DisposeAsync
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
#endif
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -338,11 +334,8 @@ public class TarArchiveAsyncTests : ArchiveTests
|
||||
{
|
||||
++numberOfEntries;
|
||||
|
||||
#if LEGACY_DOTNET
|
||||
using var tarEntryStream = await entry.OpenEntryStreamAsync();
|
||||
#else
|
||||
await using var tarEntryStream = await entry.OpenEntryStreamAsync();
|
||||
#endif
|
||||
var tarEntryStream = await entry.OpenEntryStreamAsync();
|
||||
await using var tarEntryStreamScope = tarEntryStream.DisposeAsyncScope();
|
||||
using var testFileStream = new MemoryStream();
|
||||
await tarEntryStream.CopyToAsync(testFileStream);
|
||||
Assert.Equal(testBytes.Length, testFileStream.Length);
|
||||
|
||||
@@ -47,22 +47,38 @@ public class TarReaderAsyncTests : ReaderTests
|
||||
await ReadAsync("Tar.tar.Z", CompressionType.Lzw);
|
||||
|
||||
[Theory]
|
||||
[InlineData("Tar.tar.gz", "gz", CompressionType.GZip)]
|
||||
[InlineData("Tar.tar.Z", "z", CompressionType.Lzw)]
|
||||
[InlineData("Tar.tar.gz", "gz", CompressionType.GZip, false)]
|
||||
[InlineData("Tar.tar.gz", "gz", CompressionType.GZip, true)]
|
||||
[InlineData("Tar.tar.Z", "z", CompressionType.Lzw, false)]
|
||||
[InlineData("Tar.tar.Z", "z", CompressionType.Lzw, true)]
|
||||
public async ValueTask ReaderFactory_ExtensionHint_PreservesCompressedTarDetection_Async(
|
||||
string archiveName,
|
||||
string extensionHint,
|
||||
CompressionType compressionType
|
||||
CompressionType compressionType,
|
||||
bool useForwardOnlyStream
|
||||
)
|
||||
{
|
||||
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName));
|
||||
using var file = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName));
|
||||
Stream source = useForwardOnlyStream ? new ForwardOnlyStream(file) : file;
|
||||
await using var stream = new AsyncOnlyStream(source);
|
||||
var options = ReaderOptions.ForExternalStream.WithExtensionHint(extensionHint);
|
||||
|
||||
await using var reader = await ReaderFactory.OpenAsyncReader(stream, options);
|
||||
|
||||
Assert.Equal(ArchiveType.Tar, reader.Type);
|
||||
Assert.True(await reader.MoveToNextEntryAsync());
|
||||
Assert.Equal(compressionType, reader.Entry.CompressionType);
|
||||
var entryCount = 0;
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
entryCount++;
|
||||
Assert.Equal(compressionType, reader.Entry.CompressionType);
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(entryCount > 0);
|
||||
VerifyFiles();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -337,13 +353,8 @@ public class TarReaderAsyncTests : ReaderTests
|
||||
Assert.True(await reader.MoveToNextEntryAsync());
|
||||
Assert.Equal("inner.tar.gz", reader.Entry.Key);
|
||||
|
||||
#if !LEGACY_DOTNET
|
||||
await using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
await using var flushingStream = new FlushOnDisposeStream(entryStream);
|
||||
#else
|
||||
using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
using var flushingStream = new FlushOnDisposeStream(entryStream);
|
||||
#endif
|
||||
|
||||
// Extract inner.tar.gz
|
||||
await using var innerReader = await ReaderFactory.OpenAsyncReader(flushingStream);
|
||||
|
||||
@@ -89,11 +89,8 @@ public class TarWriterNonSeekableTests
|
||||
{
|
||||
var entry = fileEntries.Single(e => e.Key == name);
|
||||
using var extracted = new MemoryStream();
|
||||
#if LEGACY_DOTNET
|
||||
using (var entryStream = await entry.OpenEntryStreamAsync())
|
||||
#else
|
||||
await using (var entryStream = await entry.OpenEntryStreamAsync())
|
||||
#endif
|
||||
var entryStream = await entry.OpenEntryStreamAsync();
|
||||
await using (entryStream.DisposeAsyncScope())
|
||||
{
|
||||
await entryStream.CopyToAsync(extracted);
|
||||
}
|
||||
|
||||
@@ -198,11 +198,7 @@ public class Zip64AsyncTests : WriterTests
|
||||
count++;
|
||||
lastKey = rd.Entry.Key;
|
||||
|
||||
#if LEGACY_DOTNET
|
||||
using var entryStream = await rd.OpenEntryStreamAsync();
|
||||
#else
|
||||
await using var entryStream = await rd.OpenEntryStreamAsync();
|
||||
#endif
|
||||
if (rd.Entry.Key == "small")
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
@@ -337,17 +333,10 @@ public class Zip64AsyncTests : WriterTests
|
||||
);
|
||||
while (await rd.MoveToNextEntryAsync())
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
using (var entryStream = await rd.OpenEntryStreamAsync())
|
||||
{
|
||||
await entryStream.SkipEntryAsync();
|
||||
}
|
||||
#else
|
||||
await using (var entryStream = await rd.OpenEntryStreamAsync())
|
||||
{
|
||||
await entryStream.SkipEntryAsync();
|
||||
}
|
||||
#endif
|
||||
count++;
|
||||
if (prev != null)
|
||||
{
|
||||
|
||||
@@ -122,11 +122,8 @@ public class ZipCrcExtractionTests : ArchiveTests
|
||||
using var zipStream = CreateZipWithInvalidCrc(useDataDescriptor: false);
|
||||
using var archive = ZipArchive.OpenArchive(zipStream);
|
||||
var entry = archive.Entries.Single(e => !e.IsDirectory);
|
||||
#if LEGACY_DOTNET
|
||||
// MemoryStream has nothing to dispose asynchronously
|
||||
using var destination = new MemoryStream();
|
||||
#else
|
||||
await using var destination = new MemoryStream();
|
||||
#endif
|
||||
|
||||
var exception = await Assert.ThrowsAsync<InvalidFormatException>(async () =>
|
||||
await entry.WriteToAsync(destination, new ExtractionOptions { CheckCrc = true })
|
||||
@@ -141,11 +138,8 @@ public class ZipCrcExtractionTests : ArchiveTests
|
||||
using var zipStream = CreateZipWithInvalidCrc(useDataDescriptor: false);
|
||||
using var archive = ZipArchive.OpenArchive(zipStream);
|
||||
var entry = archive.Entries.Single(e => !e.IsDirectory);
|
||||
#if LEGACY_DOTNET
|
||||
// MemoryStream has nothing to dispose asynchronously
|
||||
using var destination = new MemoryStream();
|
||||
#else
|
||||
await using var destination = new MemoryStream();
|
||||
#endif
|
||||
|
||||
await entry.WriteToAsync(destination, new ExtractionOptions { CheckCrc = false });
|
||||
|
||||
|
||||
@@ -315,11 +315,7 @@ public class ZipReaderAsyncTests : ReaderTests
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
#else
|
||||
await using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
#endif
|
||||
// Read some data
|
||||
var buffer = new byte[1024];
|
||||
await entryStream.ReadAsync(buffer, 0, buffer.Length);
|
||||
@@ -342,11 +338,7 @@ public class ZipReaderAsyncTests : ReaderTests
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
#else
|
||||
await using var entryStream = await reader.OpenEntryStreamAsync();
|
||||
#endif
|
||||
// Read some data
|
||||
var buffer = new byte[1024];
|
||||
await entryStream.ReadAsync(buffer, 0, buffer.Length);
|
||||
|
||||
@@ -163,11 +163,8 @@ public class ZipWriterNonSeekableTests
|
||||
{
|
||||
var entry = archive.Entries.Single(e => e.Key == name);
|
||||
using var extracted = new MemoryStream();
|
||||
#if LEGACY_DOTNET
|
||||
using (var entryStream = await entry.OpenEntryStreamAsync())
|
||||
#else
|
||||
await using (var entryStream = await entry.OpenEntryStreamAsync())
|
||||
#endif
|
||||
var entryStream = await entry.OpenEntryStreamAsync();
|
||||
await using (entryStream.DisposeAsyncScope())
|
||||
{
|
||||
await entryStream.CopyToAsync(extracted);
|
||||
}
|
||||
|
||||
BIN
tests/TestArchives/Archives/Large/Large.7z
Normal file
BIN
tests/TestArchives/Archives/Large/Large.7z
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Large/Large.gz
Normal file
BIN
tests/TestArchives/Archives/Large/Large.gz
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Large/Large.rar
Normal file
BIN
tests/TestArchives/Archives/Large/Large.rar
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Large/Large.tar.gz
Normal file
BIN
tests/TestArchives/Archives/Large/Large.tar.gz
Normal file
Binary file not shown.
BIN
tests/TestArchives/Archives/Large/Large.zip
Normal file
BIN
tests/TestArchives/Archives/Large/Large.zip
Normal file
Binary file not shown.
37
tests/TestArchives/Archives/Large/README.md
Normal file
37
tests/TestArchives/Archives/Large/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Large Test Archives
|
||||
|
||||
Each fixture contains one `large.bin` entry with a 67,108,864-byte (64 MiB)
|
||||
deterministic, repeated text pattern with a 2 KiB `0xFF` prefix. The entry CRC-32 is
|
||||
`f9081eb0`.
|
||||
|
||||
| Fixture | Archive API | Reader API |
|
||||
| --- | --- | --- |
|
||||
| `Large.zip` | Yes | Yes |
|
||||
| Generated `Large.tar` | Yes | Yes |
|
||||
| `Large.gz` | Yes | Yes |
|
||||
| `Large.rar` | Yes | Yes |
|
||||
| `Large.7z` | Yes | No |
|
||||
| `Large.tar.gz` | No | Yes |
|
||||
|
||||
The compressible payload keeps the compressed fixtures small while requiring a full
|
||||
64 MiB decompression to validate each API. The tests expand `Large.tar.gz` to a scratch
|
||||
`Large.tar` before exercising raw TAR support, so the 64 MiB TAR file is not committed.
|
||||
|
||||
## Regenerating
|
||||
|
||||
The fixtures were created with `zip`, `tar`, `gzip`, RAR 7.22, and 7-Zip. Run these
|
||||
commands from a temporary directory after replacing `<repo>` with the repository root:
|
||||
|
||||
```sh
|
||||
yes "SharpCompress large fixture" | head -c 67108864 > large.bin
|
||||
printf '\377%.0s' {1..2048} > prefix.bin
|
||||
dd if=prefix.bin of=large.bin bs=2048 count=1 conv=notrunc
|
||||
touch -t 202001010000 large.bin
|
||||
mkdir -p <repo>/tests/TestArchives/Archives/Large
|
||||
zip -X -9 -j <repo>/tests/TestArchives/Archives/Large/Large.zip large.bin
|
||||
COPYFILE_DISABLE=1 tar -cf large.tar large.bin
|
||||
gzip -9 -c large.bin > <repo>/tests/TestArchives/Archives/Large/Large.gz
|
||||
gzip -n -9 -c large.tar > <repo>/tests/TestArchives/Archives/Large/Large.tar.gz
|
||||
rar a -ma5 -m5 -ep <repo>/tests/TestArchives/Archives/Large/Large.rar large.bin
|
||||
7z a -t7z -mx=9 <repo>/tests/TestArchives/Archives/Large/Large.7z large.bin
|
||||
```
|
||||
Reference in New Issue
Block a user