diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 870092f1..94368ece 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -20,7 +20,7 @@ public static class ArchiveFactory public static IArchive Open(Stream stream, ReaderOptions? readerOptions = null) { readerOptions ??= new ReaderOptions(); - stream = new SharpCompressStream(stream, bufferSize: readerOptions.BufferSize); + stream = SharpCompressStream.Create(stream, bufferSize: readerOptions.BufferSize); return FindFactory(stream).Open(stream, readerOptions); } diff --git a/src/SharpCompress/Common/Arj/ArjEntry.cs b/src/SharpCompress/Common/Arj/ArjEntry.cs index 2e83190c..8dd84e87 100644 --- a/src/SharpCompress/Common/Arj/ArjEntry.cs +++ b/src/SharpCompress/Common/Arj/ArjEntry.cs @@ -25,7 +25,17 @@ namespace SharpCompress.Common.Arj public override long CompressedSize => _filePart?.Header.CompressedSize ?? 0; - public override CompressionType CompressionType => CompressionType.None; + public override CompressionType CompressionType + { + get + { + if (_filePart.Header.CompressionMethod == CompressionMethod.Stored) + { + return CompressionType.None; + } + return CompressionType.ArjLZ77; + } + } public override long Size => _filePart?.Header.OriginalSize ?? 0; @@ -39,7 +49,7 @@ namespace SharpCompress.Common.Arj public override bool IsEncrypted => false; - public override bool IsDirectory => _filePart.Header.FileType == (int)FileType.Directory; + public override bool IsDirectory => _filePart.Header.FileType == FileType.Directory; public override bool IsSplitAfter => false; diff --git a/src/SharpCompress/Common/Arj/ArjFilePart.cs b/src/SharpCompress/Common/Arj/ArjFilePart.cs index 24dbbab5..10fe24c6 100644 --- a/src/SharpCompress/Common/Arj/ArjFilePart.cs +++ b/src/SharpCompress/Common/Arj/ArjFilePart.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using SharpCompress.Common.Arj.Headers; +using SharpCompress.Compressors.Arj; using SharpCompress.IO; namespace SharpCompress.Common.Arj @@ -37,6 +38,18 @@ namespace SharpCompress.Common.Arj Header.CompressedSize ); break; + case CompressionMethod.CompressedFastest: + byte[] compressedData = new byte[Header.CompressedSize]; + _stream.Position = Header.DataStartPosition; + _stream.Read(compressedData, 0, compressedData.Length); + + byte[] decompressedData = LHDecoder.DecodeFastest( + compressedData, + (int)Header.OriginalSize // ARJ can only handle files up to 2GB, so casting to int should not be an issue. + ); + + compressedStream = new MemoryStream(decompressedData); + break; default: throw new NotSupportedException( "CompressionMethod: " + Header.CompressionMethod diff --git a/src/SharpCompress/Common/Arj/Headers/ArjHeader.cs b/src/SharpCompress/Common/Arj/Headers/ArjHeader.cs index cb8fc479..138f75e7 100644 --- a/src/SharpCompress/Common/Arj/Headers/ArjHeader.cs +++ b/src/SharpCompress/Common/Arj/Headers/ArjHeader.cs @@ -26,6 +26,8 @@ namespace SharpCompress.Common.Arj.Headers } public ArjHeaderType ArjHeaderType { get; } + public byte Flags { get; set; } + public FileType FileType { get; set; } public abstract ArjHeader? Read(Stream reader); @@ -34,23 +36,31 @@ namespace SharpCompress.Common.Arj.Headers // check for magic bytes Span magic = stackalloc byte[2]; if (stream.Read(magic) != 2) + { return Array.Empty(); + } var magicValue = (ushort)(magic[0] | magic[1] << 8); if (magicValue != ARJ_MAGIC) + { throw new InvalidDataException("Not an ARJ file (wrong magic bytes)"); + } // read header_size byte[] headerBytes = new byte[2]; stream.Read(headerBytes, 0, 2); var headerSize = (ushort)(headerBytes[0] | headerBytes[1] << 8); if (headerSize < 1) + { return Array.Empty(); + } var body = new byte[headerSize]; var read = stream.Read(body, 0, headerSize); if (read < headerSize) + { return Array.Empty(); + } byte[] crc = new byte[4]; read = stream.Read(crc, 0, 4); @@ -111,5 +121,22 @@ namespace SharpCompress.Common.Arj.Headers extendedHeader.Add(header); } } + + // Flag helpers + public bool IsGabled => (Flags & 0x01) != 0; + public bool IsAnsiPage => (Flags & 0x02) != 0; + public bool IsVolume => (Flags & 0x04) != 0; + public bool IsArjProtected => (Flags & 0x08) != 0; + public bool IsPathSym => (Flags & 0x10) != 0; + public bool IsBackup => (Flags & 0x20) != 0; + public bool IsSecured => (Flags & 0x40) != 0; + public bool IsAltName => (Flags & 0x80) != 0; + + public static FileType FileTypeFromByte(byte value) + { + return Enum.IsDefined(typeof(FileType), value) + ? (FileType)value + : Headers.FileType.Unknown; + } } } diff --git a/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs b/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs index 595037f5..f790b495 100644 --- a/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs +++ b/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs @@ -16,9 +16,7 @@ namespace SharpCompress.Common.Arj.Headers public byte ArchiverVersionNumber { get; set; } public byte MinVersionToExtract { get; set; } public HostOS HostOS { get; set; } - public byte ArjFlags { get; set; } public CompressionMethod CompressionMethod { get; set; } - public int FileType { get; set; } public DosDateTime DateTimeModified { get; set; } = new DosDateTime(0); public long CompressedSize { get; set; } public long OriginalSize { get; set; } @@ -36,11 +34,6 @@ namespace SharpCompress.Common.Arj.Headers private const byte StdHdrSize = 30; private const byte R9HdrSize = 46; - public bool IsGarbled => (ArjFlags & 0x01) != 0; - public bool IsVolume => (ArjFlags & 0x04) != 0; - public bool IsExtFile => (ArjFlags & 0x08) != 0; - public bool IsPathSym => (ArjFlags & 0x10) != 0; - public bool IsBackup => (ArjFlags & 0x20) != 0; public ArjLocalHeader(ArchiveEncoding archiveEncoding) : base(ArjHeaderType.LocalHeader) @@ -92,12 +85,12 @@ namespace SharpCompress.Common.Arj.Headers } byte headerSize = headerBytes[offset++]; - byte archiverVersionNumber = headerBytes[offset++]; - byte minVersionToExtract = headerBytes[offset++]; + ArchiverVersionNumber = headerBytes[offset++]; + MinVersionToExtract = headerBytes[offset++]; HostOS hostOS = (HostOS)headerBytes[offset++]; - byte arjFlags = headerBytes[offset++]; - CompressionMethod compressionMethod = CompressionMethodFromByte(headerBytes[offset++]); - FileType fileType = FileTypeFromByte(headerBytes[offset++]); + Flags = headerBytes[offset++]; + CompressionMethod = CompressionMethodFromByte(headerBytes[offset++]); + FileType = FileTypeFromByte(headerBytes[offset++]); offset++; // Skip 1 byte @@ -164,12 +157,5 @@ namespace SharpCompress.Common.Arj.Headers _ => CompressionMethod.Unknown, }; } - - public static FileType FileTypeFromByte(byte value) - { - return Enum.IsDefined(typeof(FileType), value) - ? (FileType)value - : Headers.FileType.Unknown; - } } } diff --git a/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs b/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs index f050837c..6ba2b83c 100644 --- a/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs +++ b/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs @@ -16,9 +16,7 @@ namespace SharpCompress.Common.Arj.Headers public int ArchiverVersionNumber { get; private set; } public int MinVersionToExtract { get; private set; } public HostOS HostOs { get; private set; } - public int Flags { get; private set; } public int SecurityVersion { get; private set; } - public int FileType { get; private set; } public DosDateTime CreationDateTime { get; private set; } = new DosDateTime(0); public long CompressedSize { get; private set; } public long ArchiveSize { get; private set; } @@ -51,13 +49,13 @@ namespace SharpCompress.Common.Arj.Headers { var offset = 1; - int ReadByte() + byte ReadByte() { if (offset >= headerBytes.Length) { throw new EndOfStreamException(); } - return headerBytes[offset++] & 0xFF; + return (byte)(headerBytes[offset++] & 0xFF); } int ReadInt16() @@ -116,7 +114,7 @@ namespace SharpCompress.Common.Arj.Headers Flags = ReadByte(); SecurityVersion = ReadByte(); - FileType = ReadByte(); + FileType = FileTypeFromByte(ReadByte()); offset++; // skip reserved @@ -136,15 +134,5 @@ namespace SharpCompress.Common.Arj.Headers return this; } - - // Flag helpers - public bool IsGabled => (Flags & 0x01) != 0; - public bool IsAnsiPage => (Flags & 0x02) != 0; - public bool IsVolume => (Flags & 0x04) != 0; - public bool IsArjProtected => (Flags & 0x08) != 0; - public bool IsPathSym => (Flags & 0x10) != 0; - public bool IsBackup => (Flags & 0x20) != 0; - public bool IsSecured => (Flags & 0x40) != 0; - public bool IsAltName => (Flags & 0x80) != 0; } } diff --git a/src/SharpCompress/Common/CompressionType.cs b/src/SharpCompress/Common/CompressionType.cs index 76ada667..57f65b05 100644 --- a/src/SharpCompress/Common/CompressionType.cs +++ b/src/SharpCompress/Common/CompressionType.cs @@ -29,4 +29,5 @@ public enum CompressionType Crushed, Distilled, ZStandard, + ArjLZ77, } diff --git a/src/SharpCompress/Compressors/ADC/ADCBase.cs b/src/SharpCompress/Compressors/ADC/ADCBase.cs index 6fc755a9..35301b52 100644 --- a/src/SharpCompress/Compressors/ADC/ADCBase.cs +++ b/src/SharpCompress/Compressors/ADC/ADCBase.cs @@ -24,10 +24,29 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. using System; +using System.Buffers; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.Compressors.ADC; +/// +/// Result of an ADC decompression operation +/// +public class AdcDecompressResult +{ + /// + /// Number of bytes read from input + /// + public int BytesRead { get; set; } + + /// + /// Decompressed output buffer + /// + public byte[]? Output { get; set; } +} + /// /// Provides static methods for decompressing Apple Data Compression data /// @@ -78,6 +97,173 @@ public static class ADCBase public static int Decompress(byte[] input, out byte[]? output, int bufferSize = 262144) => Decompress(new MemoryStream(input), out output, bufferSize); + /// + /// Decompresses a byte buffer asynchronously that's compressed with ADC + /// + /// Compressed buffer + /// Max size for decompressed data + /// Cancellation token + /// Result containing bytes read and decompressed data + public static async Task DecompressAsync( + byte[] input, + int bufferSize = 262144, + CancellationToken cancellationToken = default + ) => await DecompressAsync(new MemoryStream(input), bufferSize, cancellationToken); + + /// + /// Decompresses a stream asynchronously that's compressed with ADC + /// + /// Stream containing compressed data + /// Max size for decompressed data + /// Cancellation token + /// Result containing bytes read and decompressed data + public static async Task DecompressAsync( + Stream input, + int bufferSize = 262144, + CancellationToken cancellationToken = default + ) + { + var result = new AdcDecompressResult(); + + if (input is null || input.Length == 0) + { + result.BytesRead = 0; + result.Output = null; + return result; + } + + var start = (int)input.Position; + var position = (int)input.Position; + int chunkSize; + int offset; + int chunkType; + var buffer = ArrayPool.Shared.Rent(bufferSize); + var outPosition = 0; + var full = false; + byte[] temp = ArrayPool.Shared.Rent(3); + + try + { + while (position < input.Length) + { + cancellationToken.ThrowIfCancellationRequested(); + var readByte = input.ReadByte(); + if (readByte == -1) + { + break; + } + + chunkType = GetChunkType((byte)readByte); + + switch (chunkType) + { + case PLAIN: + chunkSize = GetChunkSize((byte)readByte); + if (outPosition + chunkSize > bufferSize) + { + full = true; + break; + } + + var readCount = await input.ReadAsync( + buffer, + outPosition, + chunkSize, + cancellationToken + ); + outPosition += readCount; + position += readCount + 1; + break; + case TWO_BYTE: + chunkSize = GetChunkSize((byte)readByte); + temp[0] = (byte)readByte; + temp[1] = (byte)input.ReadByte(); + offset = GetOffset(temp.AsSpan(0, 2)); + if (outPosition + chunkSize > bufferSize) + { + full = true; + break; + } + + if (offset == 0) + { + var lastByte = buffer[outPosition - 1]; + for (var i = 0; i < chunkSize; i++) + { + buffer[outPosition] = lastByte; + outPosition++; + } + + position += 2; + } + else + { + for (var i = 0; i < chunkSize; i++) + { + buffer[outPosition] = buffer[outPosition - offset - 1]; + outPosition++; + } + + position += 2; + } + + break; + case THREE_BYTE: + chunkSize = GetChunkSize((byte)readByte); + temp[0] = (byte)readByte; + temp[1] = (byte)input.ReadByte(); + temp[2] = (byte)input.ReadByte(); + offset = GetOffset(temp.AsSpan(0, 3)); + if (outPosition + chunkSize > bufferSize) + { + full = true; + break; + } + + if (offset == 0) + { + var lastByte = buffer[outPosition - 1]; + for (var i = 0; i < chunkSize; i++) + { + buffer[outPosition] = lastByte; + outPosition++; + } + + position += 3; + } + else + { + for (var i = 0; i < chunkSize; i++) + { + buffer[outPosition] = buffer[outPosition - offset - 1]; + outPosition++; + } + + position += 3; + } + + break; + } + + if (full) + { + break; + } + } + + var output = new byte[outPosition]; + Array.Copy(buffer, output, outPosition); + result.BytesRead = position - start; + result.Output = output; + return result; + } + finally + { + ArrayPool.Shared.Return(buffer); + ArrayPool.Shared.Return(temp); + } + } + /// /// Decompresses a stream that's compressed with ADC /// diff --git a/src/SharpCompress/Compressors/ADC/ADCStream.cs b/src/SharpCompress/Compressors/ADC/ADCStream.cs index 70e8fd8b..935207c9 100644 --- a/src/SharpCompress/Compressors/ADC/ADCStream.cs +++ b/src/SharpCompress/Compressors/ADC/ADCStream.cs @@ -28,6 +28,8 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.IO; namespace SharpCompress.Compressors.ADC; @@ -187,6 +189,76 @@ public sealed class ADCStream : Stream, IStreamStack return copied; } + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + if (count == 0) + { + return 0; + } + if (buffer is null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + if (offset < buffer.GetLowerBound(0)) + { + throw new ArgumentOutOfRangeException(nameof(offset)); + } + if ((offset + count) > buffer.GetLength(0)) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + if (_outBuffer is null) + { + var result = await ADCBase.DecompressAsync( + _stream, + cancellationToken: cancellationToken + ); + _outBuffer = result.Output; + _outPosition = 0; + } + + var inPosition = offset; + var toCopy = count; + var copied = 0; + + while (_outPosition + toCopy >= _outBuffer.Length) + { + cancellationToken.ThrowIfCancellationRequested(); + var piece = _outBuffer.Length - _outPosition; + Array.Copy(_outBuffer, _outPosition, buffer, inPosition, piece); + inPosition += piece; + copied += piece; + _position += piece; + toCopy -= piece; + var result = await ADCBase.DecompressAsync( + _stream, + cancellationToken: cancellationToken + ); + _outBuffer = result.Output; + _outPosition = 0; + if (result.BytesRead == 0 || _outBuffer is null || _outBuffer.Length == 0) + { + return copied; + } + } + + Array.Copy(_outBuffer, _outPosition, buffer, inPosition, toCopy); + _outPosition += toCopy; + _position += toCopy; + copied += toCopy; + return copied; + } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); diff --git a/src/SharpCompress/Compressors/Arj/DecodeFastest.cs b/src/SharpCompress/Compressors/Arj/DecodeFastest.cs new file mode 100644 index 00000000..f9f4ef44 --- /dev/null +++ b/src/SharpCompress/Compressors/Arj/DecodeFastest.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace SharpCompress.Compressors.Arj +{ + [CLSCompliant(true)] + public class BitReader + { + private readonly byte[] data; + private int bytePos = 0; + private int bitPos = 0; + + public BitReader(byte[] input) + { + data = input; + } + + public int ReadBits(int count) + { + int result = 0; + for (int i = 0; i < count; i++) + { + if (bytePos >= data.Length) + { + throw new EndOfStreamException(); + } + + int bit = (data[bytePos] >> (7 - bitPos)) & 1; + result = (result << 1) | bit; + + bitPos++; + if (bitPos == 8) + { + bitPos = 0; + bytePos++; + } + } + return result; + } + } + + [CLSCompliant(true)] + public static class LHDecoder + { + private const int THRESHOLD = 3; + + private static int DecodeVal(BitReader r, int from, int to) + { + int add = 0; + int bit = from; + + while (bit < to && r.ReadBits(1) == 1) + { + add |= 1 << bit; + bit++; + } + + int res = bit > 0 ? r.ReadBits(bit) : 0; + return res + add; + } + + public static byte[] DecodeFastest(byte[] data, int originalSize) + { + var res = new List(originalSize); + var r = new BitReader(data); + + while (res.Count < originalSize) + { + int len = DecodeVal(r, 0, 7); + if (len == 0) + { + byte nextChar = (byte)r.ReadBits(8); + res.Add(nextChar); + } + else + { + int repCount = len + THRESHOLD - 1; + int backPtr = DecodeVal(r, 9, 13); + + if (backPtr >= res.Count) + { + throw new InvalidDataException("invalid back_ptr"); + } + + int i = res.Count - 1 - backPtr; + for (int j = 0; j < repCount; j++) + { + res.Add(res[i]); + i++; + } + } + } + + return res.ToArray(); + } + } +} diff --git a/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs b/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs index 5b9884ca..e3325f27 100644 --- a/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs +++ b/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.IO; namespace SharpCompress.Compressors.BZip2; @@ -96,13 +98,37 @@ public sealed class BZip2Stream : Stream, IStreamStack public override void SetLength(long value) => stream.SetLength(value); -#if !NETFRAMEWORK&& !NETSTANDARD2_0 +#if !NETFRAMEWORK && !NETSTANDARD2_0 public override int Read(Span buffer) => stream.Read(buffer); public override void Write(ReadOnlySpan buffer) => stream.Write(buffer); + + public override async ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) => await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + + public override async ValueTask WriteAsync( + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default + ) => await stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); #endif + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) => await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) => await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + public override void Write(byte[] buffer, int offset, int count) => stream.Write(buffer, offset, count); diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs index abff4973..e466cc07 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs @@ -2,6 +2,8 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.IO; /* @@ -1127,6 +1129,28 @@ internal class CBZip2InputStream : Stream, IStreamStack return k; } + public override Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + var c = -1; + int k; + for (k = 0; k < count; ++k) + { + cancellationToken.ThrowIfCancellationRequested(); + c = ReadByte(); + if (c == -1) + { + break; + } + buffer[k + offset] = (byte)c; + } + return Task.FromResult(k); + } + public override long Seek(long offset, SeekOrigin origin) => 0; public override void SetLength(long value) { } diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs b/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs index 555c6fcb..db470ff2 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.IO; /* @@ -2022,6 +2024,21 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack } } + public override Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + for (var k = 0; k < count; ++k) + { + cancellationToken.ThrowIfCancellationRequested(); + WriteByte(buffer[k + offset]); + } + return Task.CompletedTask; + } + public override bool CanRead => false; public override bool CanSeek => false; diff --git a/src/SharpCompress/Factories/ArjFactory.cs b/src/SharpCompress/Factories/ArjFactory.cs index 597393a6..7499946b 100644 --- a/src/SharpCompress/Factories/ArjFactory.cs +++ b/src/SharpCompress/Factories/ArjFactory.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Arj.Headers; using SharpCompress.Readers; -using SharpCompress.Readers.Arc; +using SharpCompress.Readers.Arj; namespace SharpCompress.Factories { diff --git a/src/SharpCompress/Readers/Arj/ArjReader.cs b/src/SharpCompress/Readers/Arj/ArjReader.cs index 37bde917..d5441e27 100644 --- a/src/SharpCompress/Readers/Arj/ArjReader.cs +++ b/src/SharpCompress/Readers/Arj/ArjReader.cs @@ -10,7 +10,7 @@ using SharpCompress.Common.Arj.Headers; using SharpCompress.Common.Zip; using SharpCompress.Common.Zip.Headers; -namespace SharpCompress.Readers.Arc +namespace SharpCompress.Readers.Arj { public class ArjReader : AbstractReader { @@ -33,13 +33,14 @@ namespace SharpCompress.Readers.Arc protected override IEnumerable GetEntries(Stream stream) { - var headerReader = new ArjMainHeader(new ArchiveEncoding()); - var mainHeader = headerReader.Read(stream); + ArchiveEncoding encoding = new ArchiveEncoding(); + var mainHeaderReader = new ArjMainHeader(encoding); + var localHeaderReader = new ArjLocalHeader(encoding); + var mainHeader = mainHeaderReader.Read(stream); while (true) { - var localReader = new ArjLocalHeader(new ArchiveEncoding()); - var localHeader = localReader.Read(stream); + var localHeader = localHeaderReader.Read(stream); if (localHeader == null) break; diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.cs b/src/SharpCompress/Writers/Zip/ZipWriter.cs index daee1ed6..f867c8a9 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.cs @@ -160,7 +160,7 @@ public class ZipWriter : AbstractWriter WriteDirectoryEntry(normalizedName, options); } - public override async Task WriteDirectoryAsync( + public override Task WriteDirectoryAsync( string directoryName, DateTime? modificationTime, CancellationToken cancellationToken = default @@ -168,7 +168,7 @@ public class ZipWriter : AbstractWriter { // Synchronous implementation is sufficient for directory entries WriteDirectory(directoryName, modificationTime); - await Task.CompletedTask.ConfigureAwait(false); + return Task.CompletedTask; } private void WriteDirectoryEntry(string directoryPath, ZipWriterEntryOptions options) diff --git a/tests/SharpCompress.Test/AdcAsyncTest.cs b/tests/SharpCompress.Test/AdcAsyncTest.cs new file mode 100644 index 00000000..185174aa --- /dev/null +++ b/tests/SharpCompress.Test/AdcAsyncTest.cs @@ -0,0 +1,61 @@ +using System.IO; +using System.Threading.Tasks; +using SharpCompress.Compressors.ADC; +using Xunit; + +namespace SharpCompress.Test; + +public class AdcAsyncTest : TestBase +{ + [Fact] + public async Task TestAdcStreamAsyncWholeChunk() + { + using var decFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_decompressed.bin")); + var decompressed = new byte[decFs.Length]; + decFs.Read(decompressed, 0, decompressed.Length); + + using var cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin")); + using var decStream = new ADCStream(cmpFs); + var test = new byte[262144]; + + await decStream.ReadAsync(test, 0, test.Length); + + Assert.Equal(decompressed, test); + } + + [Fact] + public async Task TestAdcStreamAsync() + { + using var decFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_decompressed.bin")); + var decompressed = new byte[decFs.Length]; + decFs.Read(decompressed, 0, decompressed.Length); + + using var cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin")); + using var decStream = new ADCStream(cmpFs); + using var decMs = new MemoryStream(); + var test = new byte[512]; + var count = 0; + + do + { + count = await decStream.ReadAsync(test, 0, test.Length); + decMs.Write(test, 0, count); + } while (count > 0); + + Assert.Equal(decompressed, decMs.ToArray()); + } + + [Fact] + public async Task TestAdcStreamAsyncWithCancellation() + { + using var cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin")); + using var decStream = new ADCStream(cmpFs); + var test = new byte[512]; + using var cts = new System.Threading.CancellationTokenSource(); + + // Read should complete without cancellation + var bytesRead = await decStream.ReadAsync(test, 0, test.Length, cts.Token); + + Assert.True(bytesRead > 0); + } +} diff --git a/tests/SharpCompress.Test/ArchiveTests.cs b/tests/SharpCompress.Test/ArchiveTests.cs index 2849a71f..2f47e121 100644 --- a/tests/SharpCompress.Test/ArchiveTests.cs +++ b/tests/SharpCompress.Test/ArchiveTests.cs @@ -622,4 +622,36 @@ public class ArchiveTests : ReaderTests VerifyFiles(); } } + + [Fact] + public void ArchiveFactory_Open_WithPreWrappedStream() + { + // Test that ArchiveFactory.Open works correctly with a stream that's already wrapped + // This addresses the issue where ZIP files fail to open on Linux + var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.noEmptyDirs.zip"); + + // Open with a pre-wrapped stream + using (var fileStream = File.OpenRead(testArchive)) + using (var wrappedStream = SharpCompressStream.Create(fileStream, bufferSize: 32768)) + using (var archive = ArchiveFactory.Open(wrappedStream)) + { + Assert.Equal(ArchiveType.Zip, archive.Type); + Assert.Equal(3, archive.Entries.Count()); + } + } + + [Fact] + public void ArchiveFactory_Open_WithRawFileStream() + { + // Test that ArchiveFactory.Open works correctly with a raw FileStream + // This is the common use case reported in the issue + var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.noEmptyDirs.zip"); + + using (var stream = File.OpenRead(testArchive)) + using (var archive = ArchiveFactory.Open(stream)) + { + Assert.Equal(ArchiveType.Zip, archive.Type); + Assert.Equal(3, archive.Entries.Count()); + } + } } diff --git a/tests/SharpCompress.Test/Arj/ArjReaderTests.cs b/tests/SharpCompress.Test/Arj/ArjReaderTests.cs index 84a5e437..2d2d6536 100644 --- a/tests/SharpCompress.Test/Arj/ArjReaderTests.cs +++ b/tests/SharpCompress.Test/Arj/ArjReaderTests.cs @@ -6,7 +6,6 @@ using System.Text; using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Readers; -using SharpCompress.Readers.Arc; using Xunit; namespace SharpCompress.Test.Arj @@ -21,26 +20,7 @@ namespace SharpCompress.Test.Arj [Fact] public void Arj_Uncompressed_Read() => Read("Arj.store.arj", CompressionType.None); - - private void ProcessArchive(string archiveName) - { - // Process a given archive by its name - using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, archiveName))) - using (IReader reader = ArjReader.Open(stream)) - { - while (reader.MoveToNextEntry()) - { - if (!reader.Entry.IsDirectory) - { - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); - } - } - } - - VerifyFilesByExtension(); - } + [Fact] + public void Arj_Method4_Read() => Read("Arj.method4.arj"); } } diff --git a/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs b/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs new file mode 100644 index 00000000..c801ab0d --- /dev/null +++ b/tests/SharpCompress.Test/BZip2/BZip2StreamAsyncTests.cs @@ -0,0 +1,231 @@ +using System; +using System.Buffers; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Compressors.BZip2; +using Xunit; + +namespace SharpCompress.Test.BZip2; + +public class BZip2StreamAsyncTests +{ + private byte[] CreateTestData(int size) + { + var data = new byte[size]; + // Create compressible data with repetitive pattern + for (int i = 0; i < size; i++) + { + data[i] = (byte)('A' + (i % 26)); + } + return data; + } + + [Fact] + public async Task BZip2CompressDecompressAsyncTest() + { + var testData = CreateTestData(10000); + byte[] compressed; + + // Compress + using (var memoryStream = new MemoryStream()) + { + using ( + var bzip2Stream = new BZip2Stream( + memoryStream, + SharpCompress.Compressors.CompressionMode.Compress, + false + ) + ) + { + await bzip2Stream.WriteAsync(testData, 0, testData.Length); + (bzip2Stream as BZip2Stream)?.Finish(); + } + compressed = memoryStream.ToArray(); + } + + // Verify compression occurred + Assert.True(compressed.Length > 0); + Assert.True(compressed.Length < testData.Length); + + // Decompress + byte[] decompressed; + using (var memoryStream = new MemoryStream(compressed)) + { + using ( + var bzip2Stream = new BZip2Stream( + memoryStream, + SharpCompress.Compressors.CompressionMode.Decompress, + false + ) + ) + { + decompressed = new byte[testData.Length]; + var totalRead = 0; + int bytesRead; + while ( + ( + bytesRead = await bzip2Stream.ReadAsync( + decompressed, + totalRead, + testData.Length - totalRead + ) + ) > 0 + ) + { + totalRead += bytesRead; + } + } + } + + // Verify decompression + Assert.Equal(testData, decompressed); + } + + [Fact] + public async Task BZip2ReadAsyncWithCancellationTest() + { + var testData = Encoding.ASCII.GetBytes(new string('A', 5000)); // Repetitive data compresses well + byte[] compressed; + + // Compress + using (var memoryStream = new MemoryStream()) + { + using ( + var bzip2Stream = new BZip2Stream( + memoryStream, + SharpCompress.Compressors.CompressionMode.Compress, + false + ) + ) + { + await bzip2Stream.WriteAsync(testData, 0, testData.Length); + (bzip2Stream as BZip2Stream)?.Finish(); + } + compressed = memoryStream.ToArray(); + } + + // Decompress with cancellation support + using (var memoryStream = new MemoryStream(compressed)) + { + using ( + var bzip2Stream = new BZip2Stream( + memoryStream, + SharpCompress.Compressors.CompressionMode.Decompress, + false + ) + ) + { + var buffer = new byte[1024]; + using var cts = new System.Threading.CancellationTokenSource(); + + // Read should complete without cancellation + var bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length, cts.Token); + Assert.True(bytesRead > 0); + } + } + } + + [Fact] + public async Task BZip2MultipleAsyncWritesTest() + { + using (var memoryStream = new MemoryStream()) + { + using ( + var bzip2Stream = new BZip2Stream( + memoryStream, + SharpCompress.Compressors.CompressionMode.Compress, + false + ) + ) + { + var data1 = Encoding.ASCII.GetBytes("Hello "); + var data2 = Encoding.ASCII.GetBytes("World"); + var data3 = Encoding.ASCII.GetBytes("!"); + + await bzip2Stream.WriteAsync(data1, 0, data1.Length); + await bzip2Stream.WriteAsync(data2, 0, data2.Length); + await bzip2Stream.WriteAsync(data3, 0, data3.Length); + + (bzip2Stream as BZip2Stream)?.Finish(); + } + + var compressed = memoryStream.ToArray(); + Assert.True(compressed.Length > 0); + + // Decompress and verify + using (var readStream = new MemoryStream(compressed)) + { + using ( + var bzip2Stream = new BZip2Stream( + readStream, + SharpCompress.Compressors.CompressionMode.Decompress, + false + ) + ) + { + var result = new StringBuilder(); + var buffer = new byte[256]; + int bytesRead; + while ((bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + result.Append(Encoding.ASCII.GetString(buffer, 0, bytesRead)); + } + + Assert.Equal("Hello World!", result.ToString()); + } + } + } + } + + [Fact] + public async Task BZip2LargeDataAsyncTest() + { + var largeData = CreateTestData(100000); + + // Compress + byte[] compressed; + using (var memoryStream = new MemoryStream()) + { + using ( + var bzip2Stream = new BZip2Stream( + memoryStream, + SharpCompress.Compressors.CompressionMode.Compress, + false + ) + ) + { + await bzip2Stream.WriteAsync(largeData, 0, largeData.Length); + (bzip2Stream as BZip2Stream)?.Finish(); + } + compressed = memoryStream.ToArray(); + } + + // Decompress + byte[] decompressed; + using (var memoryStream = new MemoryStream(compressed)) + { + using ( + var bzip2Stream = new BZip2Stream( + memoryStream, + SharpCompress.Compressors.CompressionMode.Decompress, + false + ) + ) + { + decompressed = new byte[largeData.Length]; + var totalRead = 0; + int bytesRead; + var buffer = new byte[4096]; + while ((bytesRead = await bzip2Stream.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + Array.Copy(buffer, 0, decompressed, totalRead, bytesRead); + totalRead += bytesRead; + } + } + } + + // Verify + Assert.Equal(largeData, decompressed); + } +} diff --git a/tests/SharpCompress.Test/Compressors/Rar/RarCRCTest.cs b/tests/SharpCompress.Test/Rar/RarCRCTest.cs similarity index 100% rename from tests/SharpCompress.Test/Compressors/Rar/RarCRCTest.cs rename to tests/SharpCompress.Test/Rar/RarCRCTest.cs diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index b919331d..c5539fc8 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -13,29 +13,53 @@ namespace SharpCompress.Test; public abstract class ReaderTests : TestBase { + protected void Read(string testArchive, ReaderOptions? options = null) + { + ReadCore(testArchive, options, ReadImpl); + } + protected void Read( string testArchive, CompressionType expectedCompression, ReaderOptions? options = null ) { - testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); + ReadCore(testArchive, options, (path, opts) => ReadImpl(path, expectedCompression, opts)); + } - options ??= new ReaderOptions() { BufferSize = 0x20000 }; //test larger buffer size (need test rather than eyeballing debug logs :P) + private void ReadCore( + string testArchive, + ReaderOptions? options, + Action readImpl + ) + { + testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive); + options ??= new ReaderOptions { BufferSize = 0x20000 }; options.LeaveStreamOpen = true; - ReadImpl(testArchive, expectedCompression, options); + readImpl(testArchive, options); options.LeaveStreamOpen = false; - ReadImpl(testArchive, expectedCompression, options); + readImpl(testArchive, options); + VerifyFiles(); } + private void ReadImpl(string testArchive, ReaderOptions options) + { + ReadImplCore(testArchive, options, UseReader); + } + private void ReadImpl( string testArchive, CompressionType expectedCompression, ReaderOptions options ) + { + ReadImplCore(testArchive, options, r => UseReader(r, expectedCompression)); + } + + private void ReadImplCore(string testArchive, ReaderOptions options, Action useReader) { using var file = File.OpenRead(testArchive); using var protectedStream = SharpCompressStream.Create( @@ -47,19 +71,17 @@ public abstract class ReaderTests : TestBase using var testStream = new TestStream(protectedStream); using (var reader = ReaderFactory.Open(testStream, options)) { - UseReader(reader, expectedCompression); + useReader(reader); protectedStream.ThrowOnDispose = false; Assert.False(testStream.IsDisposed, $"{nameof(testStream)} prematurely closed"); } - // Boolean XOR -- If the stream should be left open (true), then the stream should not be diposed (false) - // and if the stream should be closed (false), then the stream should be disposed (true) var message = $"{nameof(options.LeaveStreamOpen)} is set to '{options.LeaveStreamOpen}', so {nameof(testStream.IsDisposed)} should be set to '{!testStream.IsDisposed}', but is set to {testStream.IsDisposed}"; Assert.True(options.LeaveStreamOpen != testStream.IsDisposed, message); } - public void UseReader(IReader reader, CompressionType expectedCompression) + protected void UseReader(IReader reader, CompressionType expectedCompression) { while (reader.MoveToNextEntry()) { @@ -74,6 +96,20 @@ public abstract class ReaderTests : TestBase } } + private void UseReader(IReader reader) + { + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + reader.WriteEntryToDirectory( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + } + } + } + protected async Task ReadAsync( string testArchive, CompressionType expectedCompression, diff --git a/tests/TestArchives/Archives/Arj.method4.arj b/tests/TestArchives/Archives/Arj.method4.arj new file mode 100644 index 00000000..5f3bbbf6 Binary files /dev/null and b/tests/TestArchives/Archives/Arj.method4.arj differ