diff --git a/src/SharpCompress/Common/Arc/ArcEntryHeader.cs b/src/SharpCompress/Common/Arc/ArcEntryHeader.cs index e50c0e49..d2c0479f 100644 --- a/src/SharpCompress/Common/Arc/ArcEntryHeader.cs +++ b/src/SharpCompress/Common/Arc/ArcEntryHeader.cs @@ -2,6 +2,8 @@ using System; using System.IO; using System.Linq; using System.Text; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.Common.Arc { @@ -32,6 +34,23 @@ namespace SharpCompress.Common.Arc return LoadFrom(headerBytes); } + public async ValueTask ReadHeaderAsync( + Stream stream, + CancellationToken cancellationToken = default + ) + { + byte[] headerBytes = new byte[29]; + if ( + await stream.ReadAsync(headerBytes, 0, headerBytes.Length, cancellationToken) + != headerBytes.Length + ) + { + return null; + } + DataStartPosition = stream.Position; + return LoadFrom(headerBytes); + } + public ArcEntryHeader LoadFrom(byte[] headerBytes) { CompressionMethod = GetCompressionType(headerBytes[1]); diff --git a/src/SharpCompress/Common/Arc/ArcFilePart.Async.cs b/src/SharpCompress/Common/Arc/ArcFilePart.Async.cs new file mode 100644 index 00000000..60f6b697 --- /dev/null +++ b/src/SharpCompress/Common/Arc/ArcFilePart.Async.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Compressors.Lzw; +using SharpCompress.Compressors.RLE90; +using SharpCompress.Compressors.Squeezed; +using SharpCompress.IO; + +namespace SharpCompress.Common.Arc +{ + public partial class ArcFilePart + { + internal override async ValueTask GetCompressedStreamAsync( + CancellationToken cancellationToken = default + ) + { + if (_stream != null) + { + Stream compressedStream; + switch (Header.CompressionMethod) + { + case CompressionType.None: + compressedStream = new ReadOnlySubStream( + _stream, + Header.DataStartPosition, + Header.CompressedSize + ); + break; + case CompressionType.Packed: + compressedStream = new RunLength90Stream( + _stream, + (int)Header.CompressedSize + ); + break; + case CompressionType.Squeezed: + compressedStream = await SqueezeStream.CreateAsync( + _stream, + (int)Header.CompressedSize, + cancellationToken + ); + break; + case CompressionType.Crunched: + if (Header.OriginalSize > 128 * 1024) + { + throw new NotSupportedException( + "CompressionMethod: " + + Header.CompressionMethod + + " with size > 128KB" + ); + } + compressedStream = new ArcLzwStream( + _stream, + (int)Header.CompressedSize, + true + ); + break; + default: + throw new NotSupportedException( + "CompressionMethod: " + Header.CompressionMethod + ); + } + return compressedStream; + } + return _stream; + } + } +} diff --git a/src/SharpCompress/Common/Arc/ArcFilePart.cs b/src/SharpCompress/Common/Arc/ArcFilePart.cs index cf9ee495..0c4474ff 100644 --- a/src/SharpCompress/Common/Arc/ArcFilePart.cs +++ b/src/SharpCompress/Common/Arc/ArcFilePart.cs @@ -15,7 +15,7 @@ using SharpCompress.IO; namespace SharpCompress.Common.Arc { - public class ArcFilePart : FilePart + public partial class ArcFilePart : FilePart { private readonly Stream? _stream; @@ -51,7 +51,10 @@ namespace SharpCompress.Common.Arc ); break; case CompressionType.Squeezed: - compressedStream = new SqueezeStream(_stream, (int)Header.CompressedSize); + compressedStream = SqueezeStream.Create( + _stream, + (int)Header.CompressedSize + ); break; case CompressionType.Crunched: if (Header.OriginalSize > 128 * 1024) diff --git a/src/SharpCompress/Compressors/Squeezed/BitReader.Async.cs b/src/SharpCompress/Compressors/Squeezed/BitReader.Async.cs new file mode 100644 index 00000000..a4c032e7 --- /dev/null +++ b/src/SharpCompress/Compressors/Squeezed/BitReader.Async.cs @@ -0,0 +1,33 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Compressors.Squeezed +{ + public partial class BitReader + { + public async ValueTask ReadBitAsync(CancellationToken cancellationToken = default) + { + if (_bitCount == 0) + { + byte[] buffer = new byte[1]; + int bytesRead = await _stream + .ReadAsync(buffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + if (bytesRead == 0) + { + throw new EndOfStreamException(); + } + + _bitBuffer = buffer[0]; + _bitCount = 8; + } + + bool bit = (_bitBuffer & 1) != 0; + _bitBuffer >>= 1; + _bitCount--; + return bit; + } + } +} diff --git a/src/SharpCompress/Compressors/Squeezed/BitReader.cs b/src/SharpCompress/Compressors/Squeezed/BitReader.cs index 04d7b7e6..6f528668 100644 --- a/src/SharpCompress/Compressors/Squeezed/BitReader.cs +++ b/src/SharpCompress/Compressors/Squeezed/BitReader.cs @@ -1,51 +1,57 @@ using System; using System.IO; -public class BitReader +namespace SharpCompress.Compressors.Squeezed { - private readonly Stream _stream; - private int _bitBuffer; - private int _bitCount; - - public BitReader(Stream stream) + public partial class BitReader { - _stream = stream; - _bitBuffer = 0; - _bitCount = 0; - } + private readonly Stream _stream; + private int _bitBuffer; + private int _bitCount; - public bool ReadBit() - { - if (_bitCount == 0) + public BitReader(Stream stream) { - int nextByte = _stream.ReadByte(); - if (nextByte == -1) + _stream = stream; + _bitBuffer = 0; + _bitCount = 0; + } + + public bool ReadBit() + { + if (_bitCount == 0) { - throw new EndOfStreamException(); + int nextByte = _stream.ReadByte(); + if (nextByte == -1) + { + throw new EndOfStreamException(); + } + + _bitBuffer = nextByte; + _bitCount = 8; } - _bitBuffer = nextByte; - _bitCount = 8; + bool bit = (_bitBuffer & 1) != 0; + _bitBuffer >>= 1; + _bitCount--; + return bit; } - bool bit = (_bitBuffer & 1) != 0; - _bitBuffer >>= 1; - _bitCount--; - return bit; - } - - public int ReadBits(int count) - { - if (count < 1 || count > 32) + public int ReadBits(int count) { - throw new ArgumentOutOfRangeException(nameof(count), "Count must be between 1 and 32."); - } + if (count < 1 || count > 32) + { + throw new ArgumentOutOfRangeException( + nameof(count), + "Count must be between 1 and 32." + ); + } - int value = 0; - for (int i = 0; i < count; i++) - { - value = (value << 1) | (ReadBit() ? 1 : 0); + int value = 0; + for (int i = 0; i < count; i++) + { + value = (value << 1) | (ReadBit() ? 1 : 0); + } + return value; } - return value; } } diff --git a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.Async.cs b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.Async.cs new file mode 100644 index 00000000..e040094a --- /dev/null +++ b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.Async.cs @@ -0,0 +1,111 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Compressors.RLE90; + +namespace SharpCompress.Compressors.Squeezed +{ + public partial class SqueezeStream + { + public static async ValueTask CreateAsync( + Stream stream, + int compressedSize, + CancellationToken cancellationToken = default + ) + { + var squeezeStream = new SqueezeStream(stream, compressedSize); + squeezeStream._decodedStream = await squeezeStream + .BuildDecodedStreamAsync(cancellationToken) + .ConfigureAwait(false); + +#if DEBUG_STREAMS + squeezeStream.DebugConstruct(typeof(SqueezeStream)); +#endif + + return squeezeStream; + } + + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + return await _decodedStream + .ReadAsync(buffer, offset, count, cancellationToken) + .ConfigureAwait(false); + } + +#if !LEGACY_DOTNET + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + return await _decodedStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + } +#endif + + private async Task BuildDecodedStreamAsync(CancellationToken cancellationToken) + { + byte[] numNodesBytes = new byte[2]; + int bytesRead = await _stream + .ReadAsync(numNodesBytes, 0, 2, cancellationToken) + .ConfigureAwait(false); + + if (bytesRead != 2) + { + return new MemoryStream(Array.Empty()); + } + + int numnodes = numNodesBytes[0] | (numNodesBytes[1] << 8); + + if (numnodes >= NUMVALS || numnodes == 0) + { + return new MemoryStream(Array.Empty()); + } + + var dnode = new int[numnodes, 2]; + for (int j = 0; j < numnodes; j++) + { + byte[] nodeBytes = new byte[4]; + bytesRead = await _stream + .ReadAsync(nodeBytes, 0, 4, cancellationToken) + .ConfigureAwait(false); + + if (bytesRead != 4) + { + throw new EndOfStreamException(); + } + + dnode[j, 0] = (short)(nodeBytes[0] | (nodeBytes[1] << 8)); + dnode[j, 1] = (short)(nodeBytes[2] | (nodeBytes[3] << 8)); + } + + var bitReader = new BitReader(_stream); + var huffmanDecoded = new MemoryStream(); + int i = 0; + + while (true) + { + bool bit = await bitReader.ReadBitAsync(cancellationToken).ConfigureAwait(false); + i = dnode[i, bit ? 1 : 0]; + if (i < 0) + { + i = -(i + 1); + if (i == SPEOF) + { + break; + } + huffmanDecoded.WriteByte((byte)i); + i = 0; + } + } + + huffmanDecoded.Position = 0; + return new RunLength90Stream(huffmanDecoded, (int)huffmanDecoded.Length); + } + } +} diff --git a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs index 553c2945..072c80f1 100644 --- a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs +++ b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs @@ -7,24 +7,31 @@ using SharpCompress.Compressors.RLE90; namespace SharpCompress.Compressors.Squeezed { [CLSCompliant(true)] - public class SqueezeStream : Stream + public partial class SqueezeStream : Stream { private readonly Stream _stream; private readonly int _compressedSize; private const int NUMVALS = 257; private const int SPEOF = 256; - private Stream _decodedStream; + private Stream _decodedStream = null!; - public SqueezeStream(Stream stream, int compressedSize) + private SqueezeStream(Stream stream, int compressedSize) { _stream = stream ?? throw new ArgumentNullException(nameof(stream)); _compressedSize = compressedSize; - _decodedStream = BuildDecodedStream(); + } + + public static SqueezeStream Create(Stream stream, int compressedSize) + { + var squeezeStream = new SqueezeStream(stream, compressedSize); + squeezeStream._decodedStream = squeezeStream.BuildDecodedStream(); #if DEBUG_STREAMS - this.DebugConstruct(typeof(SqueezeStream)); + squeezeStream.DebugConstruct(typeof(SqueezeStream)); #endif + + return squeezeStream; } protected override void Dispose(bool disposing) diff --git a/src/SharpCompress/Readers/Arc/ArcReader.Async.cs b/src/SharpCompress/Readers/Arc/ArcReader.Async.cs new file mode 100644 index 00000000..f0569d38 --- /dev/null +++ b/src/SharpCompress/Readers/Arc/ArcReader.Async.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using SharpCompress.Common.Arc; + +namespace SharpCompress.Readers.Arc +{ + public partial class ArcReader + { + protected override async IAsyncEnumerable GetEntriesAsync(Stream stream) + { + ArcEntryHeader headerReader = new ArcEntryHeader(Options.ArchiveEncoding); + ArcEntryHeader? header; + while ( + (header = await headerReader.ReadHeaderAsync(stream, CancellationToken.None)) + != null + ) + { + yield return new ArcEntry(new ArcFilePart(header, stream)); + } + } + } +}