diff --git a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs b/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs index 0fe259cc..65033298 100644 --- a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs +++ b/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs @@ -102,7 +102,9 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry stream = new RarStream( archive.UnpackV1.Value, FileHeader, - new MultiVolumeReadOnlyStream(Parts.Cast()) + await MultiVolumeReadOnlyAsyncStream.Create( + Parts.ToAsyncEnumerable().CastAsync() + ) ); } else @@ -110,7 +112,9 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry stream = new RarStream( archive.UnpackV2017.Value, FileHeader, - new MultiVolumeReadOnlyStream(Parts.Cast()) + await MultiVolumeReadOnlyAsyncStream.Create( + Parts.ToAsyncEnumerable().CastAsync() + ) ); } diff --git a/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyAsyncStream.cs b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyAsyncStream.cs new file mode 100644 index 00000000..ba06646f --- /dev/null +++ b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyAsyncStream.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Rar; +using SharpCompress.IO; + +namespace SharpCompress.Compressors.Rar; + +internal sealed class MultiVolumeReadOnlyAsyncStream : MultiVolumeReadOnlyStreamBase, IStreamStack +{ +#if DEBUG_STREAMS + long IStreamStack.InstanceId { get; set; } +#endif + int IStreamStack.DefaultBufferSize { get; set; } + + Stream IStreamStack.BaseStream() => currentStream.NotNull(); + + int IStreamStack.BufferSize + { + get => 0; + set { } + } + + int IStreamStack.BufferPosition + { + get => 0; + set { } + } + + void IStreamStack.SetPosition(long position) { } + + private long currentPosition; + private long maxPosition; + + private IAsyncEnumerator filePartEnumerator; + private Stream? currentStream; + + private MultiVolumeReadOnlyAsyncStream(IAsyncEnumerable parts) + { + filePartEnumerator = parts.GetAsyncEnumerator(); + } + + internal static async ValueTask Create( + IAsyncEnumerable parts + ) + { + var stream = new MultiVolumeReadOnlyAsyncStream(parts); + await stream.filePartEnumerator.MoveNextAsync(); + stream.InitializeNextFilePart(); + return stream; + } + +#if NET8_0_OR_GREATER + public override async ValueTask DisposeAsync() + { + await base.DisposeAsync(); + if (filePartEnumerator != null) + { + await filePartEnumerator.DisposeAsync(); + } + currentStream = null; + } +#else + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + filePartEnumerator.DisposeAsync().AsTask().GetAwaiter().GetResult(); + + currentStream = null; + } +#endif + + private void InitializeNextFilePart() + { + maxPosition = filePartEnumerator.Current.FileHeader.CompressedSize; + currentPosition = 0; + currentStream = filePartEnumerator.Current.GetCompressedStream(); + + CurrentCrc = filePartEnumerator.Current.FileHeader.FileCrc; + } + + public override int Read(byte[] buffer, int offset, int count) => + throw new NotSupportedException( + "Synchronous read is not supported in MultiVolumeReadOnlyAsyncStream." + ); + + public override async System.Threading.Tasks.Task ReadAsync( + byte[] buffer, + int offset, + int count, + System.Threading.CancellationToken cancellationToken + ) + { + var totalRead = 0; + var currentOffset = offset; + var currentCount = count; + while (currentCount > 0) + { + var readSize = currentCount; + if (currentCount > maxPosition - currentPosition) + { + readSize = (int)(maxPosition - currentPosition); + } + + var read = await currentStream + .NotNull() + .ReadAsync(buffer, currentOffset, readSize, cancellationToken) + .ConfigureAwait(false); + if (read < 0) + { + throw new EndOfStreamException(); + } + + currentPosition += read; + currentOffset += read; + currentCount -= read; + totalRead += read; + if ( + ((maxPosition - currentPosition) == 0) + && filePartEnumerator.Current.FileHeader.IsSplitAfter + ) + { + if (filePartEnumerator.Current.FileHeader.R4Salt != null) + { + throw new InvalidFormatException( + "Sharpcompress currently does not support multi-volume decryption." + ); + } + + var fileName = filePartEnumerator.Current.FileHeader.FileName; + if (!await filePartEnumerator.MoveNextAsync()) + { + throw new InvalidFormatException( + "Multi-part rar file is incomplete. Entry expects a new volume: " + + fileName + ); + } + + InitializeNextFilePart(); + } + else + { + break; + } + } + + return totalRead; + } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public override async System.Threading.Tasks.ValueTask ReadAsync( + Memory buffer, + System.Threading.CancellationToken cancellationToken = default + ) + { + var totalRead = 0; + var currentOffset = 0; + var currentCount = buffer.Length; + while (currentCount > 0) + { + var readSize = currentCount; + if (currentCount > maxPosition - currentPosition) + { + readSize = (int)(maxPosition - currentPosition); + } + + var read = await currentStream + .NotNull() + .ReadAsync(buffer.Slice(currentOffset, readSize), cancellationToken) + .ConfigureAwait(false); + if (read < 0) + { + throw new EndOfStreamException(); + } + + currentPosition += read; + currentOffset += read; + currentCount -= read; + totalRead += read; + if ( + ((maxPosition - currentPosition) == 0) + && filePartEnumerator.Current.FileHeader.IsSplitAfter + ) + { + if (filePartEnumerator.Current.FileHeader.R4Salt != null) + { + throw new InvalidFormatException( + "Sharpcompress currently does not support multi-volume decryption." + ); + } + var fileName = filePartEnumerator.Current.FileHeader.FileName; + if (!await filePartEnumerator.MoveNextAsync()) + { + throw new InvalidFormatException( + "Multi-part rar file is incomplete. Entry expects a new volume: " + + fileName + ); + } + InitializeNextFilePart(); + } + else + { + break; + } + } + return totalRead; + } +#endif + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override void Flush() { } + + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); +} diff --git a/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyStream.cs b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyStream.cs index df1c5959..8d1a0a5e 100644 --- a/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyStream.cs +++ b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyStream.cs @@ -1,5 +1,3 @@ -#nullable disable - using System; using System.Collections.Generic; using System.IO; @@ -9,20 +7,21 @@ using SharpCompress.IO; namespace SharpCompress.Compressors.Rar; -internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack +internal sealed class MultiVolumeReadOnlyStream : MultiVolumeReadOnlyStreamBase, IStreamStack { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } #endif int IStreamStack.DefaultBufferSize { get; set; } - Stream IStreamStack.BaseStream() => currentStream; + Stream IStreamStack.BaseStream() => currentStream.NotNull(); int IStreamStack.BufferSize { get => 0; set { } } + int IStreamStack.BufferPosition { get => 0; @@ -35,7 +34,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack private long maxPosition; private IEnumerator filePartEnumerator; - private Stream currentStream; + private Stream? currentStream; internal MultiVolumeReadOnlyStream(IEnumerable parts) { @@ -56,11 +55,8 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack this.DebugDispose(typeof(MultiVolumeReadOnlyStream)); #endif - if (filePartEnumerator != null) - { - filePartEnumerator.Dispose(); - filePartEnumerator = null; - } + filePartEnumerator.Dispose(); + currentStream = null; } } @@ -87,7 +83,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack readSize = (int)(maxPosition - currentPosition); } - var read = currentStream.Read(buffer, currentOffset, readSize); + var read = currentStream.NotNull().Read(buffer, currentOffset, readSize); if (read < 0) { throw new EndOfStreamException(); @@ -108,6 +104,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack "Sharpcompress currently does not support multi-volume decryption." ); } + var fileName = filePartEnumerator.Current.FileHeader.FileName; if (!filePartEnumerator.MoveNext()) { @@ -116,6 +113,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack + fileName ); } + InitializeNextFilePart(); } else @@ -123,6 +121,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack break; } } + return totalRead; } @@ -145,6 +144,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack } var read = await currentStream + .NotNull() .ReadAsync(buffer, currentOffset, readSize, cancellationToken) .ConfigureAwait(false); if (read < 0) @@ -167,6 +167,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack "Sharpcompress currently does not support multi-volume decryption." ); } + var fileName = filePartEnumerator.Current.FileHeader.FileName; if (!filePartEnumerator.MoveNext()) { @@ -175,6 +176,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack + fileName ); } + InitializeNextFilePart(); } else @@ -182,6 +184,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack break; } } + return totalRead; } @@ -203,6 +206,7 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack } var read = await currentStream + .NotNull() .ReadAsync(buffer.Slice(currentOffset, readSize), cancellationToken) .ConfigureAwait(false); if (read < 0) @@ -250,8 +254,6 @@ internal sealed class MultiVolumeReadOnlyStream : Stream, IStreamStack public override bool CanWrite => false; - public byte[] CurrentCrc { get; private set; } - public override void Flush() { } public override long Length => throw new NotSupportedException(); diff --git a/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyStreamBase.cs b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyStreamBase.cs new file mode 100644 index 00000000..d2dc9f38 --- /dev/null +++ b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyStreamBase.cs @@ -0,0 +1,8 @@ +using System.IO; + +namespace SharpCompress.Compressors.Rar; + +internal abstract class MultiVolumeReadOnlyStreamBase : Stream +{ + public byte[]? CurrentCrc { get; protected set; } +} diff --git a/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.cs b/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.cs index 693d284b..adc1c6f3 100644 --- a/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.cs +++ b/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.cs @@ -30,7 +30,7 @@ internal class RarBLAKE2spStream : RarStream, IStreamStack void IStreamStack.SetPosition(long position) { } - private readonly MultiVolumeReadOnlyStream readStream; + private readonly MultiVolumeReadOnlyStreamBase readStream; private readonly bool disableCRCCheck; const uint BLAKE2S_NUM_ROUNDS = 10; @@ -108,7 +108,7 @@ internal class RarBLAKE2spStream : RarStream, IStreamStack private RarBLAKE2spStream( IRarUnpack unpack, FileHeader fileHeader, - MultiVolumeReadOnlyStream readStream + MultiVolumeReadOnlyStreamBase readStream ) : base(unpack, fileHeader, readStream) { @@ -137,7 +137,7 @@ internal class RarBLAKE2spStream : RarStream, IStreamStack public static async Task CreateAsync( IRarUnpack unpack, FileHeader fileHeader, - MultiVolumeReadOnlyStream readStream, + MultiVolumeReadOnlyAsyncStream readStream, CancellationToken cancellationToken = default ) { diff --git a/src/SharpCompress/Compressors/Rar/RarCrcStream.cs b/src/SharpCompress/Compressors/Rar/RarCrcStream.cs index 0ec0527d..75a1381e 100644 --- a/src/SharpCompress/Compressors/Rar/RarCrcStream.cs +++ b/src/SharpCompress/Compressors/Rar/RarCrcStream.cs @@ -29,14 +29,14 @@ internal class RarCrcStream : RarStream, IStreamStack void IStreamStack.SetPosition(long position) { } - private readonly MultiVolumeReadOnlyStream readStream; + private readonly MultiVolumeReadOnlyStreamBase readStream; private uint currentCrc; private readonly bool disableCRC; private RarCrcStream( IRarUnpack unpack, FileHeader fileHeader, - MultiVolumeReadOnlyStream readStream + MultiVolumeReadOnlyStreamBase readStream ) : base(unpack, fileHeader, readStream) { @@ -62,7 +62,7 @@ internal class RarCrcStream : RarStream, IStreamStack public static async Task CreateAsync( IRarUnpack unpack, FileHeader fileHeader, - MultiVolumeReadOnlyStream readStream, + MultiVolumeReadOnlyStreamBase readStream, CancellationToken cancellationToken = default ) { @@ -92,7 +92,7 @@ internal class RarCrcStream : RarStream, IStreamStack } else if ( !disableCRC - && GetCrc() != BitConverter.ToUInt32(readStream.CurrentCrc, 0) + && GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0) && count != 0 ) { @@ -118,7 +118,7 @@ internal class RarCrcStream : RarStream, IStreamStack } else if ( !disableCRC - && GetCrc() != BitConverter.ToUInt32(readStream.CurrentCrc, 0) + && GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0) && count != 0 ) { @@ -143,7 +143,7 @@ internal class RarCrcStream : RarStream, IStreamStack } else if ( !disableCRC - && GetCrc() != BitConverter.ToUInt32(readStream.CurrentCrc, 0) + && GetCrc() != BitConverter.ToUInt32(readStream.NotNull().CurrentCrc.NotNull(), 0) && buffer.Length != 0 ) { diff --git a/src/SharpCompress/Readers/Rar/MultiVolumeRarReader.cs b/src/SharpCompress/Readers/Rar/MultiVolumeRarReader.cs index 556ca9f5..56d7996f 100644 --- a/src/SharpCompress/Readers/Rar/MultiVolumeRarReader.cs +++ b/src/SharpCompress/Readers/Rar/MultiVolumeRarReader.cs @@ -4,6 +4,8 @@ using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Rar; @@ -47,6 +49,13 @@ internal class MultiVolumeRarReader : RarReader return enumerator; } + protected override IAsyncEnumerable CreateFilePartEnumerableForCurrentEntryAsync() + { + var enumerator = new MultiVolumeStreamAsyncEnumerator(this, streams, tempStream); + tempStream = null; + return enumerator; + } + private class MultiVolumeStreamEnumerator : IEnumerable, IEnumerator { private readonly MultiVolumeRarReader reader; @@ -110,4 +119,66 @@ internal class MultiVolumeRarReader : RarReader public void Reset() { } } + + private class MultiVolumeStreamAsyncEnumerator + : IAsyncEnumerable, + IAsyncEnumerator + { + private readonly MultiVolumeRarReader reader; + private readonly IEnumerator nextReadableStreams; + private Stream tempStream; + private bool isFirst = true; + + internal MultiVolumeStreamAsyncEnumerator( + MultiVolumeRarReader r, + IEnumerator nextReadableStreams, + Stream tempStream + ) + { + reader = r; + this.nextReadableStreams = nextReadableStreams; + this.tempStream = tempStream; + } + + public FilePart Current { get; private set; } + + public async ValueTask MoveNextAsync() + { + if (isFirst) + { + Current = reader.Entry.Parts.First(); + isFirst = false; //first stream already to go + return true; + } + + if (!reader.Entry.IsSplitAfter) + { + return false; + } + if (tempStream != null) + { + await reader.LoadStreamForReadingAsync(tempStream); + tempStream = null; + } + else if (!nextReadableStreams.MoveNext()) + { + throw new MultiVolumeExtractionException( + "No stream provided when requested by MultiVolumeRarReader" + ); + } + else + { + await reader.LoadStreamForReadingAsync(nextReadableStreams.Current); + } + + Current = reader.Entry.Parts.First(); + return true; + } + + public IAsyncEnumerator GetAsyncEnumerator( + CancellationToken cancellationToken = new() + ) => this; + + public ValueTask DisposeAsync() => new(); + } } diff --git a/src/SharpCompress/Readers/Rar/RarReader.cs b/src/SharpCompress/Readers/Rar/RarReader.cs index 34c64d1c..3b0575d1 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.cs @@ -100,6 +100,9 @@ public abstract partial class RarReader : AbstractReader CreateFilePartEnumerableForCurrentEntry() => Entry.Parts; + protected virtual IAsyncEnumerable CreateFilePartEnumerableForCurrentEntryAsync() => + Entry.Parts.ToAsyncEnumerable(); + protected override EntryStream GetEntryStream() { if (Entry.IsRedir) @@ -134,8 +137,8 @@ public abstract partial class RarReader : AbstractReader() + var stream = await MultiVolumeReadOnlyAsyncStream.Create( + CreateFilePartEnumerableForCurrentEntryAsync().CastAsync() ); if (Entry.IsRarV3) {