diff --git a/Directory.Packages.props b/Directory.Packages.props index 9a51cc35..ced78445 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,6 +14,7 @@ + diff --git a/src/SharpCompress/Common/ArchiveType.cs b/src/SharpCompress/Common/ArchiveType.cs index 503804c8..9d943895 100644 --- a/src/SharpCompress/Common/ArchiveType.cs +++ b/src/SharpCompress/Common/ArchiveType.cs @@ -8,4 +8,5 @@ public enum ArchiveType SevenZip, GZip, Arc, + Arj, } diff --git a/src/SharpCompress/Common/Arj/ArjEntry.cs b/src/SharpCompress/Common/Arj/ArjEntry.cs new file mode 100644 index 00000000..8dd84e87 --- /dev/null +++ b/src/SharpCompress/Common/Arj/ArjEntry.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common.Arc; +using SharpCompress.Common.Arj.Headers; + +namespace SharpCompress.Common.Arj +{ + public class ArjEntry : Entry + { + private readonly ArjFilePart _filePart; + + internal ArjEntry(ArjFilePart filePart) + { + _filePart = filePart; + } + + public override long Crc => _filePart.Header.OriginalCrc32; + + public override string? Key => _filePart?.Header.Name; + + public override string? LinkTarget => null; + + public override long CompressedSize => _filePart?.Header.CompressedSize ?? 0; + + 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; + + public override DateTime? LastModifiedTime => _filePart.Header.DateTimeModified.DateTime; + + public override DateTime? CreatedTime => _filePart.Header.DateTimeCreated.DateTime; + + public override DateTime? LastAccessedTime => _filePart.Header.DateTimeAccessed.DateTime; + + public override DateTime? ArchivedTime => null; + + public override bool IsEncrypted => false; + + public override bool IsDirectory => _filePart.Header.FileType == FileType.Directory; + + public override bool IsSplitAfter => false; + + internal override IEnumerable Parts => _filePart.Empty(); + } +} diff --git a/src/SharpCompress/Common/Arj/ArjFilePart.cs b/src/SharpCompress/Common/Arj/ArjFilePart.cs new file mode 100644 index 00000000..10fe24c6 --- /dev/null +++ b/src/SharpCompress/Common/Arj/ArjFilePart.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +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 +{ + public class ArjFilePart : FilePart + { + private readonly Stream _stream; + internal ArjLocalHeader Header { get; set; } + + internal ArjFilePart(ArjLocalHeader localArjHeader, Stream seekableStream) + : base(localArjHeader.ArchiveEncoding) + { + _stream = seekableStream; + Header = localArjHeader; + } + + internal override string? FilePartName => Header.Name; + + internal override Stream GetCompressedStream() + { + if (_stream != null) + { + Stream compressedStream; + switch (Header.CompressionMethod) + { + case CompressionMethod.Stored: + compressedStream = new ReadOnlySubStream( + _stream, + Header.DataStartPosition, + 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 + ); + } + return compressedStream; + } + return _stream.NotNull(); + } + + internal override Stream GetRawStream() => _stream; + } +} diff --git a/src/SharpCompress/Common/Arj/ArjVolume.cs b/src/SharpCompress/Common/Arj/ArjVolume.cs new file mode 100644 index 00000000..bf3ba109 --- /dev/null +++ b/src/SharpCompress/Common/Arj/ArjVolume.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Readers; + +namespace SharpCompress.Common.Arj +{ + public class ArjVolume : Volume + { + public ArjVolume(Stream stream, ReaderOptions readerOptions, int index = 0) + : base(stream, readerOptions, index) { } + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/ArjHeader.cs b/src/SharpCompress/Common/Arj/Headers/ArjHeader.cs new file mode 100644 index 00000000..138f75e7 --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/ArjHeader.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common.Zip.Headers; +using SharpCompress.Crypto; + +namespace SharpCompress.Common.Arj.Headers +{ + public enum ArjHeaderType + { + MainHeader, + LocalHeader, + } + + public abstract class ArjHeader + { + private const int FIRST_HDR_SIZE = 34; + private const ushort ARJ_MAGIC = 0xEA60; + + public ArjHeader(ArjHeaderType type) + { + ArjHeaderType = type; + } + + public ArjHeaderType ArjHeaderType { get; } + public byte Flags { get; set; } + public FileType FileType { get; set; } + + public abstract ArjHeader? Read(Stream reader); + + public byte[] ReadHeader(Stream stream) + { + // 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); + var checksum = Crc32Stream.Compute(body); + // Compute the hash value + if (checksum != BitConverter.ToUInt32(crc, 0)) + { + throw new InvalidDataException("Header checksum is invalid"); + } + return body; + } + + protected List ReadExtendedHeaders(Stream reader) + { + List extendedHeader = new List(); + byte[] buffer = new byte[2]; + + while (true) + { + int bytesRead = reader.Read(buffer, 0, 2); + if (bytesRead < 2) + { + throw new EndOfStreamException( + "Unexpected end of stream while reading extended header size." + ); + } + + var extHeaderSize = (ushort)(buffer[0] | (buffer[1] << 8)); + if (extHeaderSize == 0) + { + return extendedHeader; + } + + byte[] header = new byte[extHeaderSize]; + bytesRead = reader.Read(header, 0, extHeaderSize); + if (bytesRead < extHeaderSize) + { + throw new EndOfStreamException( + "Unexpected end of stream while reading extended header data." + ); + } + + byte[] crc = new byte[4]; + bytesRead = reader.Read(crc, 0, 4); + if (bytesRead < 4) + { + throw new EndOfStreamException( + "Unexpected end of stream while reading extended header CRC." + ); + } + + var checksum = Crc32Stream.Compute(header); + if (checksum != BitConverter.ToUInt32(crc, 0)) + { + throw new InvalidDataException("Extended header checksum is invalid"); + } + + 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 new file mode 100644 index 00000000..f790b495 --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace SharpCompress.Common.Arj.Headers +{ + public class ArjLocalHeader : ArjHeader + { + public ArchiveEncoding ArchiveEncoding { get; } + public long DataStartPosition { get; protected set; } + + public byte ArchiverVersionNumber { get; set; } + public byte MinVersionToExtract { get; set; } + public HostOS HostOS { get; set; } + public CompressionMethod CompressionMethod { get; set; } + public DosDateTime DateTimeModified { get; set; } = new DosDateTime(0); + public long CompressedSize { get; set; } + public long OriginalSize { get; set; } + public long OriginalCrc32 { get; set; } + public int FileSpecPosition { get; set; } + public int FileAccessMode { get; set; } + public byte FirstChapter { get; set; } + public byte LastChapter { get; set; } + public long ExtendedFilePosition { get; set; } + public DosDateTime DateTimeAccessed { get; set; } = new DosDateTime(0); + public DosDateTime DateTimeCreated { get; set; } = new DosDateTime(0); + public long OriginalSizeEvenForVolumes { get; set; } + public string Name { get; set; } = string.Empty; + public string Comment { get; set; } = string.Empty; + + private const byte StdHdrSize = 30; + private const byte R9HdrSize = 46; + + public ArjLocalHeader(ArchiveEncoding archiveEncoding) + : base(ArjHeaderType.LocalHeader) + { + ArchiveEncoding = + archiveEncoding ?? throw new ArgumentNullException(nameof(archiveEncoding)); + } + + public override ArjHeader? Read(Stream stream) + { + var body = ReadHeader(stream); + if (body.Length > 0) + { + ReadExtendedHeaders(stream); + var header = LoadFrom(body); + header.DataStartPosition = stream.Position; + return header; + } + return null; + } + + public ArjLocalHeader LoadFrom(byte[] headerBytes) + { + int offset = 0; + + int ReadInt16() + { + if (offset + 1 >= headerBytes.Length) + { + throw new EndOfStreamException(); + } + var v = headerBytes[offset] & 0xFF | (headerBytes[offset + 1] & 0xFF) << 8; + offset += 2; + return v; + } + long ReadInt32() + { + if (offset + 3 >= headerBytes.Length) + { + throw new EndOfStreamException(); + } + long v = + headerBytes[offset] & 0xFF + | (headerBytes[offset + 1] & 0xFF) << 8 + | (headerBytes[offset + 2] & 0xFF) << 16 + | (headerBytes[offset + 3] & 0xFF) << 24; + offset += 4; + return v; + } + + byte headerSize = headerBytes[offset++]; + ArchiverVersionNumber = headerBytes[offset++]; + MinVersionToExtract = headerBytes[offset++]; + HostOS hostOS = (HostOS)headerBytes[offset++]; + Flags = headerBytes[offset++]; + CompressionMethod = CompressionMethodFromByte(headerBytes[offset++]); + FileType = FileTypeFromByte(headerBytes[offset++]); + + offset++; // Skip 1 byte + + var rawTimestamp = ReadInt32(); + DateTimeModified = + rawTimestamp != 0 ? new DosDateTime(rawTimestamp) : new DosDateTime(0); + + CompressedSize = ReadInt32(); + OriginalSize = ReadInt32(); + OriginalCrc32 = ReadInt32(); + FileSpecPosition = ReadInt16(); + FileAccessMode = ReadInt16(); + + FirstChapter = headerBytes[offset++]; + LastChapter = headerBytes[offset++]; + + ExtendedFilePosition = 0; + OriginalSizeEvenForVolumes = 0; + + if (headerSize > StdHdrSize) + { + ExtendedFilePosition = ReadInt32(); + + if (headerSize >= R9HdrSize) + { + rawTimestamp = ReadInt32(); + DateTimeAccessed = + rawTimestamp != 0 ? new DosDateTime(rawTimestamp) : new DosDateTime(0); + rawTimestamp = ReadInt32(); + DateTimeCreated = + rawTimestamp != 0 ? new DosDateTime(rawTimestamp) : new DosDateTime(0); + OriginalSizeEvenForVolumes = ReadInt32(); + } + } + + Name = Encoding.ASCII.GetString( + headerBytes, + offset, + Array.IndexOf(headerBytes, (byte)0, offset) - offset + ); + offset += Name.Length + 1; + + Comment = Encoding.ASCII.GetString( + headerBytes, + offset, + Array.IndexOf(headerBytes, (byte)0, offset) - offset + ); + offset += Comment.Length + 1; + + return this; + } + + public static CompressionMethod CompressionMethodFromByte(byte value) + { + return value switch + { + 0 => CompressionMethod.Stored, + 1 => CompressionMethod.CompressedMost, + 2 => CompressionMethod.Compressed, + 3 => CompressionMethod.CompressedFaster, + 4 => CompressionMethod.CompressedFastest, + 8 => CompressionMethod.NoDataNoCrc, + 9 => CompressionMethod.NoData, + _ => CompressionMethod.Unknown, + }; + } + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs b/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs new file mode 100644 index 00000000..6ba2b83c --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs @@ -0,0 +1,138 @@ +using System; +using System.IO; +using System.Text; +using SharpCompress.Compressors.Deflate; +using SharpCompress.Crypto; + +namespace SharpCompress.Common.Arj.Headers +{ + public class ArjMainHeader : ArjHeader + { + private const int FIRST_HDR_SIZE = 34; + private const ushort ARJ_MAGIC = 0xEA60; + + public ArchiveEncoding ArchiveEncoding { get; } + + public int ArchiverVersionNumber { get; private set; } + public int MinVersionToExtract { get; private set; } + public HostOS HostOs { get; private set; } + public int SecurityVersion { get; private set; } + public DosDateTime CreationDateTime { get; private set; } = new DosDateTime(0); + public long CompressedSize { get; private set; } + public long ArchiveSize { get; private set; } + public long SecurityEnvelope { get; private set; } + public int FileSpecPosition { get; private set; } + public int SecurityEnvelopeLength { get; private set; } + public int EncryptionVersion { get; private set; } + public int LastChapter { get; private set; } + + public int ArjProtectionFactor { get; private set; } + public int Flags2 { get; private set; } + public string Name { get; private set; } = string.Empty; + public string Comment { get; private set; } = string.Empty; + + public ArjMainHeader(ArchiveEncoding archiveEncoding) + : base(ArjHeaderType.MainHeader) + { + ArchiveEncoding = + archiveEncoding ?? throw new ArgumentNullException(nameof(archiveEncoding)); + } + + public override ArjHeader? Read(Stream stream) + { + var body = ReadHeader(stream); + ReadExtendedHeaders(stream); + return LoadFrom(body); + } + + public ArjMainHeader LoadFrom(byte[] headerBytes) + { + var offset = 1; + + byte ReadByte() + { + if (offset >= headerBytes.Length) + { + throw new EndOfStreamException(); + } + return (byte)(headerBytes[offset++] & 0xFF); + } + + int ReadInt16() + { + if (offset + 1 >= headerBytes.Length) + { + throw new EndOfStreamException(); + } + var v = headerBytes[offset] & 0xFF | (headerBytes[offset + 1] & 0xFF) << 8; + offset += 2; + return v; + } + + long ReadInt32() + { + if (offset + 3 >= headerBytes.Length) + { + throw new EndOfStreamException(); + } + long v = + headerBytes[offset] & 0xFF + | (headerBytes[offset + 1] & 0xFF) << 8 + | (headerBytes[offset + 2] & 0xFF) << 16 + | (headerBytes[offset + 3] & 0xFF) << 24; + offset += 4; + return v; + } + string ReadNullTerminatedString(byte[] x, int startIndex) + { + var result = new StringBuilder(); + int i = startIndex; + + while (i < x.Length && x[i] != 0) + { + result.Append((char)x[i]); + i++; + } + + // Skip the null terminator + i++; + if (i < x.Length) + { + byte[] remainder = new byte[x.Length - i]; + Array.Copy(x, i, remainder, 0, remainder.Length); + x = remainder; + } + + return result.ToString(); + } + + ArchiverVersionNumber = ReadByte(); + MinVersionToExtract = ReadByte(); + + var hostOsByte = ReadByte(); + HostOs = hostOsByte <= 11 ? (HostOS)hostOsByte : HostOS.Unknown; + + Flags = ReadByte(); + SecurityVersion = ReadByte(); + FileType = FileTypeFromByte(ReadByte()); + + offset++; // skip reserved + + CreationDateTime = new DosDateTime((int)ReadInt32()); + CompressedSize = ReadInt32(); + ArchiveSize = ReadInt32(); + + SecurityEnvelope = ReadInt32(); + FileSpecPosition = ReadInt16(); + SecurityEnvelopeLength = ReadInt16(); + + EncryptionVersion = ReadByte(); + LastChapter = ReadByte(); + + Name = ReadNullTerminatedString(headerBytes, offset); + Comment = ReadNullTerminatedString(headerBytes, offset + 1 + Name.Length); + + return this; + } + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/CompressionMethod.cs b/src/SharpCompress/Common/Arj/Headers/CompressionMethod.cs new file mode 100644 index 00000000..acdf2ce9 --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/CompressionMethod.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SharpCompress.Common.Arj.Headers +{ + public enum CompressionMethod + { + Stored = 0, + CompressedMost = 1, + Compressed = 2, + CompressedFaster = 3, + CompressedFastest = 4, + NoDataNoCrc = 8, + NoData = 9, + Unknown, + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/DosDateTime.cs b/src/SharpCompress/Common/Arj/Headers/DosDateTime.cs new file mode 100644 index 00000000..d51045ad --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/DosDateTime.cs @@ -0,0 +1,37 @@ +using System; + +namespace SharpCompress.Common.Arj.Headers +{ + public class DosDateTime + { + public DateTime DateTime { get; } + + public DosDateTime(long dosValue) + { + // Ensure only the lower 32 bits are used + int value = unchecked((int)(dosValue & 0xFFFFFFFF)); + + var date = (value >> 16) & 0xFFFF; + var time = value & 0xFFFF; + + var day = date & 0x1F; + var month = (date >> 5) & 0x0F; + var year = ((date >> 9) & 0x7F) + 1980; + + var second = (time & 0x1F) * 2; + var minute = (time >> 5) & 0x3F; + var hour = (time >> 11) & 0x1F; + + try + { + DateTime = new DateTime(year, month, day, hour, minute, second); + } + catch + { + DateTime = DateTime.MinValue; + } + } + + public override string ToString() => DateTime.ToString("yyyy-MM-dd HH:mm:ss"); + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/FileType.cs b/src/SharpCompress/Common/Arj/Headers/FileType.cs new file mode 100644 index 00000000..39c529f0 --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/FileType.cs @@ -0,0 +1,13 @@ +namespace SharpCompress.Common.Arj.Headers +{ + public enum FileType : byte + { + Binary = 0, + Text7Bit = 1, + CommentHeader = 2, + Directory = 3, + VolumeLabel = 4, + ChapterLabel = 5, + Unknown = 255, + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/HostOS.cs b/src/SharpCompress/Common/Arj/Headers/HostOS.cs new file mode 100644 index 00000000..a74e7b0f --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/HostOS.cs @@ -0,0 +1,19 @@ +namespace SharpCompress.Common.Arj.Headers +{ + public enum HostOS + { + MsDos = 0, + PrimOS = 1, + Unix = 2, + Amiga = 3, + MacOs = 4, + OS2 = 5, + AppleGS = 6, + AtariST = 7, + NeXT = 8, + VaxVMS = 9, + Win95 = 10, + Win32 = 11, + Unknown = 255, + } +} 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/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/LZMA/AesDecoderStream.cs b/src/SharpCompress/Compressors/LZMA/AesDecoderStream.cs index a7a8583d..199f277b 100644 --- a/src/SharpCompress/Compressors/LZMA/AesDecoderStream.cs +++ b/src/SharpCompress/Compressors/LZMA/AesDecoderStream.cs @@ -2,6 +2,8 @@ using System; using System.IO; using System.Security.Cryptography; using System.Text; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Compressors.LZMA.Utilites; using SharpCompress.IO; @@ -283,5 +285,70 @@ internal sealed class AesDecoderStream : DecoderStream2, IStreamStack return count; } + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + if (count == 0 || mWritten == mLimit) + { + return 0; + } + + if (mUnderflow > 0) + { + return HandleUnderflow(buffer, offset, count); + } + + // Need at least 16 bytes to proceed. + if (mEnding - mOffset < 16) + { + Buffer.BlockCopy(mBuffer, mOffset, mBuffer, 0, mEnding - mOffset); + mEnding -= mOffset; + mOffset = 0; + + do + { + cancellationToken.ThrowIfCancellationRequested(); + var read = await mStream + .ReadAsync(mBuffer, mEnding, mBuffer.Length - mEnding, cancellationToken) + .ConfigureAwait(false); + if (read == 0) + { + // We are not done decoding and have less than 16 bytes. + throw new EndOfStreamException(); + } + + mEnding += read; + } while (mEnding - mOffset < 16); + } + + // We shouldn't return more data than we are limited to. + if (count > mLimit - mWritten) + { + count = (int)(mLimit - mWritten); + } + + // We cannot transform less than 16 bytes into the target buffer, + // but we also cannot return zero, so we need to handle this. + if (count < 16) + { + return HandleUnderflow(buffer, offset, count); + } + + if (count > mEnding - mOffset) + { + count = mEnding - mOffset; + } + + // Otherwise we transform directly into the target buffer. + var processed = mDecoder.TransformBlock(mBuffer, mOffset, count & ~15, buffer, offset); + mOffset += processed; + mWritten += processed; + return processed; + } + #endregion } diff --git a/src/SharpCompress/Compressors/LZMA/Bcj2DecoderStream.cs b/src/SharpCompress/Compressors/LZMA/Bcj2DecoderStream.cs index de2d284f..05fa8b9a 100644 --- a/src/SharpCompress/Compressors/LZMA/Bcj2DecoderStream.cs +++ b/src/SharpCompress/Compressors/LZMA/Bcj2DecoderStream.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.IO; namespace SharpCompress.Compressors.LZMA; @@ -191,6 +193,18 @@ internal class Bcj2DecoderStream : DecoderStream2, IStreamStack return count; } + public override Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + // Bcj2DecoderStream uses complex state machine with multiple streams + return Task.FromResult(Read(buffer, offset, count)); + } + public override int ReadByte() { if (_mFinished) diff --git a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs index 6b9c525f..c65cc683 100644 --- a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs +++ b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs @@ -3,6 +3,8 @@ using System; using System.Buffers; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.Compressors.LZMA.LZ; @@ -85,6 +87,12 @@ internal class OutWindow : IDisposable _stream = null; } + public async Task ReleaseStreamAsync(CancellationToken cancellationToken = default) + { + await FlushAsync(cancellationToken).ConfigureAwait(false); + _stream = null; + } + private void Flush() { if (_stream is null) @@ -104,6 +112,27 @@ internal class OutWindow : IDisposable _streamPos = _pos; } + private async Task FlushAsync(CancellationToken cancellationToken = default) + { + if (_stream is null) + { + return; + } + var size = _pos - _streamPos; + if (size == 0) + { + return; + } + await _stream + .WriteAsync(_buffer, _streamPos, size, cancellationToken) + .ConfigureAwait(false); + if (_pos >= _windowSize) + { + _pos = 0; + } + _streamPos = _pos; + } + public void CopyPending() { if (_pendingLen < 1) @@ -124,6 +153,26 @@ internal class OutWindow : IDisposable _pendingLen = rem; } + public async Task CopyPendingAsync(CancellationToken cancellationToken = default) + { + if (_pendingLen < 1) + { + return; + } + var rem = _pendingLen; + var pos = (_pendingDist < _pos ? _pos : _pos + _windowSize) - _pendingDist - 1; + while (rem > 0 && HasSpace) + { + if (pos >= _windowSize) + { + pos = 0; + } + await PutByteAsync(_buffer[pos++], cancellationToken).ConfigureAwait(false); + rem--; + } + _pendingLen = rem; + } + public void CopyBlock(int distance, int len) { var rem = len; @@ -157,6 +206,43 @@ internal class OutWindow : IDisposable _pendingDist = distance; } + public async Task CopyBlockAsync( + int distance, + int len, + CancellationToken cancellationToken = default + ) + { + var rem = len; + var pos = (distance < _pos ? _pos : _pos + _windowSize) - distance - 1; + var targetSize = HasSpace ? (int)Math.Min(rem, _limit - _total) : 0; + var sizeUntilWindowEnd = Math.Min(_windowSize - _pos, _windowSize - pos); + var sizeUntilOverlap = Math.Abs(pos - _pos); + var fastSize = Math.Min(Math.Min(sizeUntilWindowEnd, sizeUntilOverlap), targetSize); + if (fastSize >= 2) + { + _buffer.AsSpan(pos, fastSize).CopyTo(_buffer.AsSpan(_pos, fastSize)); + _pos += fastSize; + pos += fastSize; + _total += fastSize; + if (_pos >= _windowSize) + { + await FlushAsync(cancellationToken).ConfigureAwait(false); + } + rem -= fastSize; + } + while (rem > 0 && HasSpace) + { + if (pos >= _windowSize) + { + pos = 0; + } + await PutByteAsync(_buffer[pos++], cancellationToken).ConfigureAwait(false); + rem--; + } + _pendingLen = rem; + _pendingDist = distance; + } + public void PutByte(byte b) { _buffer[_pos++] = b; @@ -167,6 +253,16 @@ internal class OutWindow : IDisposable } } + public async Task PutByteAsync(byte b, CancellationToken cancellationToken = default) + { + _buffer[_pos++] = b; + _total++; + if (_pos >= _windowSize) + { + await FlushAsync(cancellationToken).ConfigureAwait(false); + } + } + public byte GetByte(int distance) { var pos = _pos - distance - 1; @@ -207,6 +303,44 @@ internal class OutWindow : IDisposable return len - size; } + public async Task CopyStreamAsync( + Stream stream, + int len, + CancellationToken cancellationToken = default + ) + { + var size = len; + while (size > 0 && _pos < _windowSize && _total < _limit) + { + cancellationToken.ThrowIfCancellationRequested(); + + var curSize = _windowSize - _pos; + if (curSize > _limit - _total) + { + curSize = (int)(_limit - _total); + } + if (curSize > size) + { + curSize = size; + } + var numReadBytes = await stream + .ReadAsync(_buffer, _pos, curSize, cancellationToken) + .ConfigureAwait(false); + if (numReadBytes == 0) + { + throw new DataErrorException(); + } + size -= numReadBytes; + _pos += numReadBytes; + _total += numReadBytes; + if (_pos >= _windowSize) + { + await FlushAsync(cancellationToken).ConfigureAwait(false); + } + } + return len - size; + } + public void SetLimit(long size) => _limit = _total + size; public bool HasSpace => _pos < _windowSize && _total < _limit; diff --git a/src/SharpCompress/Compressors/LZMA/LZipStream.cs b/src/SharpCompress/Compressors/LZMA/LZipStream.cs index 26d34d3a..e7059003 100644 --- a/src/SharpCompress/Compressors/LZMA/LZipStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LZipStream.cs @@ -1,6 +1,8 @@ using System; using System.Buffers.Binary; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Crypto; using SharpCompress.IO; @@ -157,6 +159,11 @@ public sealed class LZipStream : Stream, IStreamStack #if !NETFRAMEWORK && !NETSTANDARD2_0 + public override ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) => _stream.ReadAsync(buffer, cancellationToken); + public override int Read(Span buffer) => _stream.Read(buffer); public override void Write(ReadOnlySpan buffer) @@ -179,6 +186,25 @@ public sealed class LZipStream : Stream, IStreamStack ++_writeCount; } + public override Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) => _stream.ReadAsync(buffer, offset, count, cancellationToken); + + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + cancellationToken.ThrowIfCancellationRequested(); + await _stream.WriteAsync(buffer, offset, count, cancellationToken); + _writeCount += count; + } + #endregion /// diff --git a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs index 4baa9061..fed40533 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs @@ -1,6 +1,7 @@ #nullable disable using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using SharpCompress.Compressors.LZMA.LZ; using SharpCompress.Compressors.LZMA.RangeCoder; @@ -199,6 +200,9 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream } } +#if !NETFRAMEWORK && !NETSTANDARD2_0 + [MemberNotNull(nameof(_outWindow))] +#endif private void CreateDictionary() { if (_dictionarySize < 0) @@ -309,6 +313,42 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream _outWindow = null; } + public async System.Threading.Tasks.Task CodeAsync( + Stream inStream, + Stream outStream, + long inSize, + long outSize, + ICodeProgress progress, + System.Threading.CancellationToken cancellationToken = default + ) + { + if (_outWindow is null) + { + CreateDictionary(); + } + _outWindow.Init(outStream); + if (outSize > 0) + { + _outWindow.SetLimit(outSize); + } + else + { + _outWindow.SetLimit(long.MaxValue - _outWindow.Total); + } + + var rangeDecoder = new RangeCoder.Decoder(); + rangeDecoder.Init(inStream); + + await CodeAsync(_dictionarySize, _outWindow, rangeDecoder, cancellationToken) + .ConfigureAwait(false); + + await _outWindow.ReleaseStreamAsync(cancellationToken).ConfigureAwait(false); + rangeDecoder.ReleaseStream(); + + _outWindow.Dispose(); + _outWindow = null; + } + internal bool Code(int dictionarySize, OutWindow outWindow, RangeCoder.Decoder rangeDecoder) { var dictionarySizeCheck = Math.Max(dictionarySize, 1); @@ -435,6 +475,143 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream return false; } + internal async System.Threading.Tasks.Task CodeAsync( + int dictionarySize, + OutWindow outWindow, + RangeCoder.Decoder rangeDecoder, + System.Threading.CancellationToken cancellationToken = default + ) + { + var dictionarySizeCheck = Math.Max(dictionarySize, 1); + + await outWindow.CopyPendingAsync(cancellationToken).ConfigureAwait(false); + + while (outWindow.HasSpace) + { + cancellationToken.ThrowIfCancellationRequested(); + + var posState = (uint)outWindow.Total & _posStateMask; + if ( + _isMatchDecoders[(_state._index << Base.K_NUM_POS_STATES_BITS_MAX) + posState] + .Decode(rangeDecoder) == 0 + ) + { + byte b; + var prevByte = outWindow.GetByte(0); + if (!_state.IsCharState()) + { + b = _literalDecoder.DecodeWithMatchByte( + rangeDecoder, + (uint)outWindow.Total, + prevByte, + outWindow.GetByte((int)_rep0) + ); + } + else + { + b = _literalDecoder.DecodeNormal(rangeDecoder, (uint)outWindow.Total, prevByte); + } + await outWindow.PutByteAsync(b, cancellationToken).ConfigureAwait(false); + _state.UpdateChar(); + } + else + { + uint len; + if (_isRepDecoders[_state._index].Decode(rangeDecoder) == 1) + { + if (_isRepG0Decoders[_state._index].Decode(rangeDecoder) == 0) + { + if ( + _isRep0LongDecoders[ + (_state._index << Base.K_NUM_POS_STATES_BITS_MAX) + posState + ] + .Decode(rangeDecoder) == 0 + ) + { + _state.UpdateShortRep(); + await outWindow + .PutByteAsync(outWindow.GetByte((int)_rep0), cancellationToken) + .ConfigureAwait(false); + continue; + } + } + else + { + uint distance; + if (_isRepG1Decoders[_state._index].Decode(rangeDecoder) == 0) + { + distance = _rep1; + } + else + { + if (_isRepG2Decoders[_state._index].Decode(rangeDecoder) == 0) + { + distance = _rep2; + } + else + { + distance = _rep3; + _rep3 = _rep2; + } + _rep2 = _rep1; + } + _rep1 = _rep0; + _rep0 = distance; + } + len = _repLenDecoder.Decode(rangeDecoder, posState) + Base.K_MATCH_MIN_LEN; + _state.UpdateRep(); + } + else + { + _rep3 = _rep2; + _rep2 = _rep1; + _rep1 = _rep0; + len = Base.K_MATCH_MIN_LEN + _lenDecoder.Decode(rangeDecoder, posState); + _state.UpdateMatch(); + var posSlot = _posSlotDecoder[Base.GetLenToPosState(len)].Decode(rangeDecoder); + if (posSlot >= Base.K_START_POS_MODEL_INDEX) + { + var numDirectBits = (int)((posSlot >> 1) - 1); + _rep0 = ((2 | (posSlot & 1)) << numDirectBits); + if (posSlot < Base.K_END_POS_MODEL_INDEX) + { + _rep0 += BitTreeDecoder.ReverseDecode( + _posDecoders, + _rep0 - posSlot - 1, + rangeDecoder, + numDirectBits + ); + } + else + { + _rep0 += ( + rangeDecoder.DecodeDirectBits(numDirectBits - Base.K_NUM_ALIGN_BITS) + << Base.K_NUM_ALIGN_BITS + ); + _rep0 += _posAlignDecoder.ReverseDecode(rangeDecoder); + } + } + else + { + _rep0 = posSlot; + } + } + if (_rep0 >= outWindow.Total || _rep0 >= dictionarySizeCheck) + { + if (_rep0 == 0xFFFFFFFF) + { + return true; + } + throw new DataErrorException(); + } + await outWindow + .CopyBlockAsync((int)_rep0, (int)len, cancellationToken) + .ConfigureAwait(false); + } + } + return false; + } + public void SetDecoderProperties(byte[] properties) { if (properties.Length < 1) @@ -470,29 +647,4 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream } _outWindow.Train(stream); } - - /* - public override bool CanRead { get { return true; }} - public override bool CanWrite { get { return true; }} - public override bool CanSeek { get { return true; }} - public override long Length { get { return 0; }} - public override long Position - { - get { return 0; } - set { } - } - public override void Flush() { } - public override int Read(byte[] buffer, int offset, int count) - { - return 0; - } - public override void Write(byte[] buffer, int offset, int count) - { - } - public override long Seek(long offset, System.IO.SeekOrigin origin) - { - return 0; - } - public override void SetLength(long value) {} - */ } diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs index 4d80704e..eaef5fd3 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs @@ -3,6 +3,8 @@ using System; using System.Buffers.Binary; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Compressors.LZMA.LZ; using SharpCompress.IO; @@ -423,6 +425,82 @@ public class LzmaStream : Stream, IStreamStack } } + private async Task DecodeChunkHeaderAsync(CancellationToken cancellationToken = default) + { + var controlBuffer = new byte[1]; + await _inputStream.ReadAsync(controlBuffer, 0, 1, cancellationToken).ConfigureAwait(false); + var control = controlBuffer[0]; + _inputPosition++; + + if (control == 0x00) + { + _endReached = true; + return; + } + + if (control >= 0xE0 || control == 0x01) + { + _needProps = true; + _needDictReset = false; + _outWindow.Reset(); + } + else if (_needDictReset) + { + throw new DataErrorException(); + } + + if (control >= 0x80) + { + _uncompressedChunk = false; + + _availableBytes = (control & 0x1F) << 16; + var buffer = new byte[2]; + await _inputStream.ReadAsync(buffer, 0, 2, cancellationToken).ConfigureAwait(false); + _availableBytes += (buffer[0] << 8) + buffer[1] + 1; + _inputPosition += 2; + + await _inputStream.ReadAsync(buffer, 0, 2, cancellationToken).ConfigureAwait(false); + _rangeDecoderLimit = (buffer[0] << 8) + buffer[1] + 1; + _inputPosition += 2; + + if (control >= 0xC0) + { + _needProps = false; + await _inputStream + .ReadAsync(controlBuffer, 0, 1, cancellationToken) + .ConfigureAwait(false); + Properties[0] = controlBuffer[0]; + _inputPosition++; + + _decoder = new Decoder(); + _decoder.SetDecoderProperties(Properties); + } + else if (_needProps) + { + throw new DataErrorException(); + } + else if (control >= 0xA0) + { + _decoder = new Decoder(); + _decoder.SetDecoderProperties(Properties); + } + + _rangeDecoder.Init(_inputStream); + } + else if (control > 0x02) + { + throw new DataErrorException(); + } + else + { + _uncompressedChunk = true; + var buffer = new byte[2]; + await _inputStream.ReadAsync(buffer, 0, 2, cancellationToken).ConfigureAwait(false); + _availableBytes = (buffer[0] << 8) + buffer[1] + 1; + _inputPosition += 2; + } + } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); @@ -435,5 +513,128 @@ public class LzmaStream : Stream, IStreamStack } } + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + if (_endReached) + { + return 0; + } + + var total = 0; + while (total < count) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (_availableBytes == 0) + { + if (_isLzma2) + { + await DecodeChunkHeaderAsync(cancellationToken).ConfigureAwait(false); + } + else + { + _endReached = true; + } + if (_endReached) + { + break; + } + } + + var toProcess = count - total; + if (toProcess > _availableBytes) + { + toProcess = (int)_availableBytes; + } + + _outWindow.SetLimit(toProcess); + if (_uncompressedChunk) + { + _inputPosition += await _outWindow + .CopyStreamAsync(_inputStream, toProcess, cancellationToken) + .ConfigureAwait(false); + } + else if ( + await _decoder + .CodeAsync(_dictionarySize, _outWindow, _rangeDecoder, cancellationToken) + .ConfigureAwait(false) + && _outputSize < 0 + ) + { + _availableBytes = _outWindow.AvailableBytes; + } + + var read = _outWindow.Read(buffer, offset, toProcess); + total += read; + offset += read; + _position += read; + _availableBytes -= read; + + if (_availableBytes == 0 && !_uncompressedChunk) + { + if ( + !_rangeDecoder.IsFinished + || (_rangeDecoderLimit >= 0 && _rangeDecoder._total != _rangeDecoderLimit) + ) + { + _outWindow.SetLimit(toProcess + 1); + if ( + !await _decoder + .CodeAsync( + _dictionarySize, + _outWindow, + _rangeDecoder, + cancellationToken + ) + .ConfigureAwait(false) + ) + { + _rangeDecoder.ReleaseStream(); + throw new DataErrorException(); + } + } + + _rangeDecoder.ReleaseStream(); + + _inputPosition += _rangeDecoder._total; + if (_outWindow.HasPending) + { + throw new DataErrorException(); + } + } + } + + if (_endReached) + { + if (_inputSize >= 0 && _inputPosition != _inputSize) + { + throw new DataErrorException(); + } + if (_outputSize >= 0 && _position != _outputSize) + { + throw new DataErrorException(); + } + } + + return total; + } + + public override Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + cancellationToken.ThrowIfCancellationRequested(); + Write(buffer, offset, count); + return Task.CompletedTask; + } + public byte[] Properties { get; } = new byte[5]; } diff --git a/src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.cs b/src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.cs index 6b379792..015ff744 100644 --- a/src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.cs +++ b/src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.IO; namespace SharpCompress.Compressors.LZMA.Utilites; @@ -101,4 +103,22 @@ internal class CrcBuilderStream : Stream, IStreamStack _mCrc = Crc.Update(_mCrc, buffer, offset, count); _mTarget.Write(buffer, offset, count); } + + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + if (_mFinished) + { + throw new InvalidOperationException("CRC calculation has been finished."); + } + + Processed += count; + _mCrc = Crc.Update(_mCrc, buffer, offset, count); + await _mTarget.WriteAsync(buffer, offset, count, cancellationToken); + } } diff --git a/src/SharpCompress/Compressors/LZMA/Utilites/CrcCheckStream.cs b/src/SharpCompress/Compressors/LZMA/Utilites/CrcCheckStream.cs index c10efda1..c90ebfda 100644 --- a/src/SharpCompress/Compressors/LZMA/Utilites/CrcCheckStream.cs +++ b/src/SharpCompress/Compressors/LZMA/Utilites/CrcCheckStream.cs @@ -1,6 +1,9 @@ using System; +using System.Buffers; using System.Diagnostics; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.Compressors.LZMA.Utilites; @@ -11,7 +14,7 @@ public class CrcCheckStream : Stream private uint _mCurrentCrc; private bool _mClosed; - private readonly long[] _mBytes = new long[256]; + private readonly long[] _mBytes = ArrayPool.Shared.Rent(256); private long _mLength; public CrcCheckStream(uint crc) @@ -65,6 +68,7 @@ public class CrcCheckStream : Stream finally { base.Dispose(disposing); + ArrayPool.Shared.Return(_mBytes); } } @@ -101,4 +105,16 @@ public class CrcCheckStream : Stream _mCurrentCrc = Crc.Update(_mCurrentCrc, buffer, offset, count); } + + public override Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + cancellationToken.ThrowIfCancellationRequested(); + Write(buffer, offset, count); + return Task.CompletedTask; + } } diff --git a/src/SharpCompress/Factories/ArjFactory.cs b/src/SharpCompress/Factories/ArjFactory.cs new file mode 100644 index 00000000..7499946b --- /dev/null +++ b/src/SharpCompress/Factories/ArjFactory.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Arj.Headers; +using SharpCompress.Readers; +using SharpCompress.Readers.Arj; + +namespace SharpCompress.Factories +{ + public class ArjFactory : Factory, IReaderFactory + { + public override string Name => "Arj"; + + public override ArchiveType? KnownArchiveType => ArchiveType.Arj; + + public override IEnumerable GetSupportedExtensions() + { + yield return "arj"; + } + + public override bool IsArchive( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize + ) + { + var arjHeader = new ArjMainHeader(new ArchiveEncoding()); + if (arjHeader.Read(stream) == null) + { + return false; + } + return true; + } + + public IReader OpenReader(Stream stream, ReaderOptions? options) => + ArjReader.Open(stream, options); + } +} diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index 2462f614..dba20177 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -18,6 +18,7 @@ public abstract class Factory : IFactory RegisterFactory(new GZipFactory()); RegisterFactory(new TarFactory()); RegisterFactory(new ArcFactory()); + RegisterFactory(new ArjFactory()); } private static readonly HashSet _factories = new(); diff --git a/src/SharpCompress/Readers/Arj/ArjReader.cs b/src/SharpCompress/Readers/Arj/ArjReader.cs new file mode 100644 index 00000000..d5441e27 --- /dev/null +++ b/src/SharpCompress/Readers/Arj/ArjReader.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Common.Arj; +using SharpCompress.Common.Arj.Headers; +using SharpCompress.Common.Zip; +using SharpCompress.Common.Zip.Headers; + +namespace SharpCompress.Readers.Arj +{ + public class ArjReader : AbstractReader + { + private ArjReader(Stream stream, ReaderOptions options) + : base(options, ArchiveType.Arj) => Volume = new ArjVolume(stream, options, 0); + + public override ArjVolume Volume { get; } + + /// + /// Opens an ArjReader for Non-seeking usage with a single volume + /// + /// + /// + /// + public static ArjReader Open(Stream stream, ReaderOptions? options = null) + { + stream.NotNull(nameof(stream)); + return new ArjReader(stream, options ?? new ReaderOptions()); + } + + protected override IEnumerable GetEntries(Stream stream) + { + ArchiveEncoding encoding = new ArchiveEncoding(); + var mainHeaderReader = new ArjMainHeader(encoding); + var localHeaderReader = new ArjLocalHeader(encoding); + var mainHeader = mainHeaderReader.Read(stream); + + while (true) + { + var localHeader = localHeaderReader.Read(stream); + if (localHeader == null) + break; + + yield return new ArjEntry(new ArjFilePart((ArjLocalHeader)localHeader, stream)); + } + } + } +} diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index 97846380..80995e41 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -70,7 +70,7 @@ public static class ReaderFactory } throw new InvalidFormatException( - "Cannot determine compressed stream type. Supported Reader Formats: Arc, Zip, GZip, BZip2, Tar, Rar, LZip, XZ, ZStandard" + "Cannot determine compressed stream type. Supported Reader Formats: Arc, Arj, Zip, GZip, BZip2, Tar, Rar, LZip, XZ, ZStandard" ); } } diff --git a/src/SharpCompress/SharpCompress.csproj b/src/SharpCompress/SharpCompress.csproj index 7c8294c5..97a31080 100644 --- a/src/SharpCompress/SharpCompress.csproj +++ b/src/SharpCompress/SharpCompress.csproj @@ -38,6 +38,9 @@ + + + diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index b85a38f7..3d0d443d 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -335,9 +335,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.20, )", - "resolved": "8.0.20", - "contentHash": "Rhcto2AjGvTO62+/VTmBpumBOmqIGp7nYEbTbmEXkCq4yPGxV8whju3/HsIA/bKyo2+DggaYk5+/8sxb1AbPTw==" + "requested": "[8.0.21, )", + "resolved": "8.0.21", + "contentHash": "s8H5PZQs50OcNkaB6Si54+v3GWM7vzs6vxFRMlD3aXsbM+aPCtod62gmK0BYWou9diGzmo56j8cIf/PziijDqQ==" }, "Microsoft.SourceLink.GitHub": { "type": "Direct", diff --git a/tests/SharpCompress.Test/Arj/ArjReaderTests.cs b/tests/SharpCompress.Test/Arj/ArjReaderTests.cs new file mode 100644 index 00000000..ee9cecca --- /dev/null +++ b/tests/SharpCompress.Test/Arj/ArjReaderTests.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Readers; +using Xunit; + +namespace SharpCompress.Test.Arj +{ + public class ArjReaderTests : ReaderTests + { + public ArjReaderTests() + { + UseExtensionInsteadOfNameToVerify = true; + UseCaseInsensitiveToVerify = true; + } + + [Fact] + public void Arj_Uncompressed_Read() => Read("Arj.store.arj", CompressionType.None); + + [Fact] + public void Arj_Method4_Read() => Read("Arj.method4.arj"); + } +} 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/SharpCompress.Test/Streams/LzmaStreamAsyncTests.cs b/tests/SharpCompress.Test/Streams/LzmaStreamAsyncTests.cs new file mode 100644 index 00000000..9f19401d --- /dev/null +++ b/tests/SharpCompress.Test/Streams/LzmaStreamAsyncTests.cs @@ -0,0 +1,606 @@ +using System; +using System.Buffers; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.Compressors.LZMA; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class LzmaStreamAsyncTests +{ + [Fact] + public async Task TestLzma2Decompress1ByteAsync() + { + var properties = new byte[] { 0x01 }; + var compressedData = new byte[] { 0x01, 0x00, 0x00, 0x58, 0x00 }; + var lzma2Stream = new MemoryStream(compressedData); + + var decompressor = new LzmaStream(properties, lzma2Stream, 5, 1); + var buffer = new byte[1]; + var bytesRead = await decompressor.ReadAsync(buffer, 0, 1).ConfigureAwait(false); + Assert.Equal(1, bytesRead); + Assert.Equal((byte)'X', buffer[0]); + } + + private static byte[] LzmaData { get; } = + [ + 0x5D, + 0x00, + 0x20, + 0x00, + 0x00, + 0x48, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x80, + 0x24, + 0x18, + 0x2F, + 0xEB, + 0x20, + 0x78, + 0xBA, + 0x78, + 0x70, + 0xDC, + 0x43, + 0x2C, + 0x32, + 0xC9, + 0xC3, + 0x97, + 0x4D, + 0x10, + 0x74, + 0xE2, + 0x20, + 0xBF, + 0x5A, + 0xB4, + 0xB3, + 0xC4, + 0x31, + 0x80, + 0x26, + 0x3E, + 0x6A, + 0xEA, + 0x51, + 0xFC, + 0xE4, + 0x8D, + 0x54, + 0x96, + 0x05, + 0xCC, + 0x78, + 0x59, + 0xAC, + 0xD4, + 0x21, + 0x65, + 0x8F, + 0xA9, + 0xC8, + 0x0D, + 0x9B, + 0xE2, + 0xC2, + 0xF9, + 0x7C, + 0x3C, + 0xDD, + 0x4D, + 0x38, + 0x04, + 0x0B, + 0xF8, + 0x0B, + 0x68, + 0xA5, + 0x93, + 0x6C, + 0x64, + 0xAC, + 0xCF, + 0x71, + 0x68, + 0xE8, + 0x69, + 0x25, + 0xC6, + 0x17, + 0x28, + 0xF1, + 0x7C, + 0xF1, + 0xDC, + 0x47, + 0x51, + 0x4D, + 0x1E, + 0x0E, + 0x0B, + 0x80, + 0x37, + 0x24, + 0x58, + 0x80, + 0xF7, + 0xB4, + 0xAC, + 0x54, + 0xF1, + 0x0F, + 0x7F, + 0x0F, + 0x0F, + 0xF5, + 0x9C, + 0xDE, + 0x54, + 0x4F, + 0xA3, + 0x7B, + 0x20, + 0xC5, + 0xA8, + 0x18, + 0x3B, + 0xED, + 0xDC, + 0x04, + 0xF6, + 0xFB, + 0x86, + 0xE0, + 0xAB, + 0xB6, + 0x87, + 0x99, + 0x92, + 0x43, + 0x7B, + 0x2C, + 0xCC, + 0x31, + 0x83, + 0x90, + 0xFF, + 0xF1, + 0x76, + 0x03, + 0x90, + ]; + + /// + /// The decoded data for . + /// + private static byte[] LzmaResultData { get; } = + [ + 0x01, + 0x00, + 0xFD, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFA, + 0x61, + 0x18, + 0x5F, + 0x02, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x02, + 0x00, + 0x00, + 0x00, + 0x03, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x02, + 0x00, + 0xB4, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x3D, + 0x61, + 0xE5, + 0x5E, + 0x03, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x12, + 0x00, + 0x00, + 0x00, + 0x02, + 0x00, + 0xB4, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0xE2, + 0x61, + 0x18, + 0x5F, + 0x04, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x12, + 0x00, + 0x00, + 0x00, + 0x29, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0xFD, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x14, + 0x62, + 0x18, + 0x5F, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x03, + 0x00, + 0x00, + 0x00, + 0x40, + 0x00, + 0x00, + 0x00, + 0x09, + 0x00, + 0x00, + 0x00, + 0x02, + 0x00, + 0xB4, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x7F, + 0x61, + 0xE5, + 0x5E, + 0x05, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x3B, + 0x00, + 0x00, + 0x00, + 0xCB, + 0x15, + 0x00, + 0x00, + 0x02, + 0x00, + 0xB4, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x7F, + 0x61, + 0xE5, + 0x5E, + 0x06, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x3B, + 0x00, + 0x00, + 0x00, + 0xCB, + 0x15, + 0x00, + 0x00, + 0x02, + 0x00, + 0xB4, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x3D, + 0x61, + 0xE5, + 0x5E, + 0x07, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x12, + 0x00, + 0x00, + 0x00, + 0x02, + 0x00, + 0xB4, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0xFC, + 0x96, + 0x40, + 0x5C, + 0x08, + 0x00, + 0x00, + 0x00, + 0x60, + 0x00, + 0x00, + 0x00, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0x00, + 0x00, + 0x00, + 0x00, + 0xF8, + 0x83, + 0x12, + 0x00, + 0xD4, + 0x99, + 0x00, + 0x00, + 0x43, + 0x95, + 0x00, + 0x00, + 0xEB, + 0x7A, + 0x00, + 0x00, + 0x40, + 0x6F, + 0x00, + 0x00, + 0xD2, + 0x6F, + 0x00, + 0x00, + 0x67, + 0x74, + 0x00, + 0x00, + 0x02, + 0x69, + 0x00, + 0x00, + 0x76, + 0x79, + 0x00, + 0x00, + 0x98, + 0x66, + 0x00, + 0x00, + 0x23, + 0x25, + 0x00, + 0x00, + 0x01, + 0x00, + 0xFD, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x3B, + 0x2F, + 0xC0, + 0x5F, + 0x09, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x03, + 0x00, + 0x00, + 0x00, + 0x69, + 0x00, + 0x3D, + 0x00, + 0x0A, + 0x00, + 0x00, + 0x00, + ]; + + [Fact] + public async Task TestLzmaBufferAsync() + { + var input = new MemoryStream(LzmaData); + using var output = new MemoryStream(); + var properties = new byte[5]; + await input.ReadAsync(properties, 0, 5).ConfigureAwait(false); + + var fileLengthBytes = new byte[8]; + await input.ReadAsync(fileLengthBytes, 0, 8).ConfigureAwait(false); + var fileLength = BitConverter.ToInt64(fileLengthBytes, 0); + + var coder = new Decoder(); + coder.SetDecoderProperties(properties); + coder.Code(input, output, input.Length, fileLength, null); + + Assert.Equal(output.ToArray(), LzmaResultData); + } + + [Fact] + public async Task TestLzmaStreamEncodingWritesDataAsync() + { + using var inputStream = new MemoryStream(LzmaResultData); + using MemoryStream outputStream = new(); + using var lzmaStream = new LzmaStream(LzmaEncoderProperties.Default, false, outputStream); + await inputStream.CopyToAsync(lzmaStream).ConfigureAwait(false); + lzmaStream.Close(); + Assert.NotEqual(0, outputStream.Length); + } + + [Fact] + public async Task TestLzmaEncodingAccuracyAsync() + { + var input = new MemoryStream(LzmaResultData); + var compressed = new MemoryStream(); + var lzmaEncodingStream = new LzmaStream(LzmaEncoderProperties.Default, false, compressed); + await input.CopyToAsync(lzmaEncodingStream).ConfigureAwait(false); + lzmaEncodingStream.Close(); + compressed.Position = 0; + + var output = new MemoryStream(); + await DecompressLzmaStreamAsync( + lzmaEncodingStream.Properties, + compressed, + compressed.Length, + output, + LzmaResultData.LongLength + ) + .ConfigureAwait(false); + + Assert.Equal(output.ToArray(), LzmaResultData); + } + + private static async Task DecompressLzmaStreamAsync( + byte[] properties, + Stream compressedStream, + long compressedSize, + Stream decompressedStream, + long decompressedSize + ) + { + var lzmaStream = new LzmaStream( + properties, + compressedStream, + compressedSize, + -1, + null, + false + ); + + var buffer = new byte[1024]; + long totalRead = 0; + while (totalRead < decompressedSize) + { + var toRead = (int)Math.Min(buffer.Length, decompressedSize - totalRead); + var read = await lzmaStream.ReadAsync(buffer, 0, toRead).ConfigureAwait(false); + if (read > 0) + { + await decompressedStream.WriteAsync(buffer, 0, read).ConfigureAwait(false); + totalRead += read; + } + else + { + break; + } + } + } +} diff --git a/tests/SharpCompress.Test/Streams/LzmaStreamTests.cs b/tests/SharpCompress.Test/Streams/LzmaStreamTests.cs index fd909b79..5b00140b 100644 --- a/tests/SharpCompress.Test/Streams/LzmaStreamTests.cs +++ b/tests/SharpCompress.Test/Streams/LzmaStreamTests.cs @@ -581,7 +581,7 @@ public class LzmaStreamTests false ); - var buffer = ArrayPool.Shared.Rent(1024); + var buffer = new byte[1024]; long totalRead = 0; while (totalRead < decompressedSize) { @@ -597,6 +597,5 @@ public class LzmaStreamTests break; } } - ArrayPool.Shared.Return(buffer); } } diff --git a/tests/SharpCompress.Test/Streams/RewindableStreamAsyncTest.cs b/tests/SharpCompress.Test/Streams/RewindableStreamAsyncTest.cs new file mode 100644 index 00000000..3893f539 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/RewindableStreamAsyncTest.cs @@ -0,0 +1,96 @@ +using System.IO; +using System.Threading.Tasks; +using SharpCompress.IO; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class RewindableStreamAsyncTest +{ + [Fact] + public async Task TestRewindAsync() + { + var ms = new MemoryStream(); + var bw = new BinaryWriter(ms); + bw.Write(1); + bw.Write(2); + bw.Write(3); + bw.Write(4); + bw.Write(5); + bw.Write(6); + bw.Write(7); + bw.Flush(); + ms.Position = 0; + var stream = new SharpCompressStream(ms, bufferSize: 0x10000); + + Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); + + ((IStreamStack)stream).StackSeek(0); + long pos = stream.Position; + Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(5, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(6, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(7, await ReadInt32Async(stream).ConfigureAwait(false)); + + ((IStreamStack)stream).StackSeek(pos); + Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); + } + + [Fact] + public async Task TestIncompleteRewindAsync() + { + var ms = new MemoryStream(); + var bw = new BinaryWriter(ms); + bw.Write(1); + bw.Write(2); + bw.Write(3); + bw.Write(4); + bw.Write(5); + bw.Write(6); + bw.Write(7); + bw.Flush(); + ms.Position = 0; + var stream = new SharpCompressStream(ms, bufferSize: 0x10000); + + Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); + ((IStreamStack)stream).StackSeek(0); + + Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); + long pos = stream.Position; + + Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(5, await ReadInt32Async(stream).ConfigureAwait(false)); + ((IStreamStack)stream).StackSeek(pos); + + Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(5, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(6, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(7, await ReadInt32Async(stream).ConfigureAwait(false)); + } + + private static async Task ReadInt32Async(Stream stream) + { + var buffer = new byte[4]; + var bytesRead = await stream.ReadAsync(buffer, 0, 4).ConfigureAwait(false); + if (bytesRead != 4) + { + throw new EndOfStreamException(); + } + return buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24); + } +} diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs index a33560a6..d806246d 100644 --- a/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs @@ -28,148 +28,111 @@ public class SharpCompressStreamAsyncTests [Fact] public async Task BufferReadAsyncTest() { - byte[] data = ArrayPool.Shared.Rent(0x100000); - byte[] test = ArrayPool.Shared.Rent(0x1000); - try + byte[] data = new byte[0x100000]; + byte[] test = new byte[0x1000]; + using (MemoryStream ms = new MemoryStream(data)) { - using (MemoryStream ms = new MemoryStream(data)) + CreateData(ms); + + using (SharpCompressStream scs = new SharpCompressStream(ms, true, false, 0x10000)) { - CreateData(ms); + scs.Seek(0x1000, SeekOrigin.Begin); + Assert.Equal(0x1000, scs.Position); // position in the SharpCompressionStream + Assert.Equal(0x1000, ms.Position); // initial seek + full buffer read - using (SharpCompressStream scs = new SharpCompressStream(ms, true, false, 0x10000)) - { - IStreamStack stack = (IStreamStack)scs; + await scs.ReadAsync(test, 0, test.Length); // read bytes 0x1000 to 0x2000 + Assert.Equal(0x2000, scs.Position); // stream has correct position + Assert.True(data.Skip(test.Length).Take(test.Length).SequenceEqual(test)); // is the data correct + Assert.Equal(0x11000, ms.Position); // seek plus read bytes - scs.Seek(0x1000, SeekOrigin.Begin); - Assert.Equal(0x1000, scs.Position); // position in the SharpCompressionStream - Assert.Equal(0x1000, ms.Position); // initial seek + full buffer read - - await scs.ReadAsync(test, 0, test.Length); // read bytes 0x1000 to 0x2000 - Assert.Equal(0x2000, scs.Position); // stream has correct position - Assert.True(data.Skip(test.Length).Take(test.Length).SequenceEqual(test)); // is the data correct - Assert.Equal(0x11000, ms.Position); // seek plus read bytes - - scs.Seek(0x500, SeekOrigin.Begin); // seek before the buffer start - await scs.ReadAsync(test, 0, test.Length); // read bytes 0x500 to 0x1500 - Assert.Equal(0x1500, scs.Position); // stream has correct position - Assert.True(data.Skip(0x500).Take(test.Length).SequenceEqual(test)); // is the data correct - Assert.Equal(0x10500, ms.Position); // seek plus read bytes - } + scs.Seek(0x500, SeekOrigin.Begin); // seek before the buffer start + await scs.ReadAsync(test, 0, test.Length); // read bytes 0x500 to 0x1500 + Assert.Equal(0x1500, scs.Position); // stream has correct position + Assert.True(data.Skip(0x500).Take(test.Length).SequenceEqual(test)); // is the data correct + Assert.Equal(0x10500, ms.Position); // seek plus read bytes } } - finally - { - ArrayPool.Shared.Return(data); - ArrayPool.Shared.Return(test); - } } [Fact] public async Task BufferReadAndSeekAsyncTest() { - byte[] data = ArrayPool.Shared.Rent(0x100000); - byte[] test = ArrayPool.Shared.Rent(0x1000); - try + byte[] data = new byte[0x100000]; + byte[] test = new byte[0x1000]; + using (MemoryStream ms = new MemoryStream(data)) { - using (MemoryStream ms = new MemoryStream(data)) + CreateData(ms); + + using (SharpCompressStream scs = new SharpCompressStream(ms, true, false, 0x10000)) { - CreateData(ms); + IStreamStack stack = (IStreamStack)scs; - using (SharpCompressStream scs = new SharpCompressStream(ms, true, false, 0x10000)) - { - IStreamStack stack = (IStreamStack)scs; + await scs.ReadAsync(test, 0, test.Length); // read bytes 0 to 0x1000 + Assert.True(data.Take(test.Length).SequenceEqual(test)); // is the data correct + Assert.Equal(0x1000, scs.Position); // stream has correct position + Assert.Equal(0x10000, ms.Position); // moved the base stream on by buffer size - await scs.ReadAsync(test, 0, test.Length); // read bytes 0 to 0x1000 - Assert.True(data.Take(test.Length).SequenceEqual(test)); // is the data correct - Assert.Equal(0x1000, scs.Position); // stream has correct position - Assert.Equal(0x10000, ms.Position); // moved the base stream on by buffer size + await scs.ReadAsync(test, 0, test.Length); // read bytes 0x1000 to 0x2000 + Assert.Equal(0x2000, scs.Position); // stream has correct position + Assert.True(data.Skip(test.Length).Take(test.Length).SequenceEqual(test)); // is the data correct + Assert.Equal(0x10000, ms.Position); // the base stream has not moved - await scs.ReadAsync(test, 0, test.Length); // read bytes 0x1000 to 0x2000 - Assert.Equal(0x2000, scs.Position); // stream has correct position - Assert.True(data.Skip(test.Length).Take(test.Length).SequenceEqual(test)); // is the data correct - Assert.Equal(0x10000, ms.Position); // the base stream has not moved + // rewind the buffer + stack.Rewind(0x1000); // rewind buffer back by 0x1000 bytes - // rewind the buffer - stack.Rewind(0x1000); // rewind buffer back by 0x1000 bytes - - // repeat the previous test - await scs.ReadAsync(test, 0, test.Length); // read bytes 0x1000 to 0x2000 - Assert.Equal(0x2000, scs.Position); // stream has correct position - Assert.True(data.Skip(test.Length).Take(test.Length).SequenceEqual(test)); // is the data correct - Assert.Equal(0x10000, ms.Position); // the base stream has not moved - } + // repeat the previous test + await scs.ReadAsync(test, 0, test.Length); // read bytes 0x1000 to 0x2000 + Assert.Equal(0x2000, scs.Position); // stream has correct position + Assert.True(data.Skip(test.Length).Take(test.Length).SequenceEqual(test)); // is the data correct + Assert.Equal(0x10000, ms.Position); // the base stream has not moved } } - finally - { - ArrayPool.Shared.Return(data); - ArrayPool.Shared.Return(test); - } } [Fact] public async Task MultipleAsyncReadsTest() { - byte[] data = ArrayPool.Shared.Rent(0x100000); - byte[] test1 = ArrayPool.Shared.Rent(0x800); - byte[] test2 = ArrayPool.Shared.Rent(0x800); - try + byte[] data = new byte[0x100000]; + byte[] test1 = new byte[0x800]; + byte[] test2 = new byte[0x800]; + using (MemoryStream ms = new MemoryStream(data)) { - using (MemoryStream ms = new MemoryStream(data)) + CreateData(ms); + + using (SharpCompressStream scs = new SharpCompressStream(ms, true, false, 0x10000)) { - CreateData(ms); + // Read first chunk + await scs.ReadAsync(test1, 0, test1.Length); + Assert.Equal(0x800, scs.Position); + Assert.True(data.Take(test1.Length).SequenceEqual(test1)); // first read is correct - using (SharpCompressStream scs = new SharpCompressStream(ms, true, false, 0x10000)) - { - // Read first chunk - await scs.ReadAsync(test1, 0, test1.Length); - Assert.Equal(0x800, scs.Position); - Assert.True(data.Take(test1.Length).SequenceEqual(test1)); // first read is correct - - // Read second chunk - await scs.ReadAsync(test2, 0, test2.Length); - Assert.Equal(0x1000, scs.Position); - Assert.True(data.Skip(test1.Length).Take(test2.Length).SequenceEqual(test2)); // second read is correct - } + // Read second chunk + await scs.ReadAsync(test2, 0, test2.Length); + Assert.Equal(0x1000, scs.Position); + Assert.True(data.Skip(test1.Length).Take(test2.Length).SequenceEqual(test2)); // second read is correct } } - finally - { - ArrayPool.Shared.Return(data); - ArrayPool.Shared.Return(test1); - ArrayPool.Shared.Return(test2); - } } [Fact] public async Task LargeBufferAsyncReadTest() { - byte[] data = ArrayPool.Shared.Rent(0x200000); - byte[] test = ArrayPool.Shared.Rent(0x8000); - try + byte[] data = new byte[0x200000]; + byte[] test = new byte[0x8000]; + using (MemoryStream ms = new MemoryStream(data)) { - using (MemoryStream ms = new MemoryStream(data)) - { - CreateData(ms); + CreateData(ms); - using (SharpCompressStream scs = new SharpCompressStream(ms, true, false, 0x10000)) + using (SharpCompressStream scs = new SharpCompressStream(ms, true, false, 0x10000)) + { + for (int i = 0; i < 10; i++) { - for (int i = 0; i < 10; i++) - { - await scs.ReadAsync(test, 0, test.Length); - long expectedPosition = (long)(i + 1) * test.Length; - Assert.Equal(expectedPosition, scs.Position); - Assert.True( - data.Skip(i * test.Length).Take(test.Length).SequenceEqual(test) - ); - } + await scs.ReadAsync(test, 0, test.Length); + long expectedPosition = (long)(i + 1) * test.Length; + Assert.Equal(expectedPosition, scs.Position); + Assert.True(data.Skip(i * test.Length).Take(test.Length).SequenceEqual(test)); } } } - finally - { - ArrayPool.Shared.Return(data); - ArrayPool.Shared.Return(test); - } } } diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamTest.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamTest.cs index 5161076b..1c4a8b57 100644 --- a/tests/SharpCompress.Test/Streams/SharpCompressStreamTest.cs +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamTest.cs @@ -27,8 +27,8 @@ public class SharpCompressStreamTests [Fact] public void BufferReadTest() { - byte[] data = ArrayPool.Shared.Rent(0x100000); - byte[] test = ArrayPool.Shared.Rent(0x1000); + byte[] data = new byte[0x100000]; + byte[] test = new byte[0x1000]; using (MemoryStream ms = new MemoryStream(data)) { createData(ms); @@ -53,16 +53,13 @@ public class SharpCompressStreamTests Assert.Equal(0x10500, ms.Position); //seek plus read bytes } } - - ArrayPool.Shared.Return(data); - ArrayPool.Shared.Return(test); } [Fact] public void BufferReadAndSeekTest() { - byte[] data = ArrayPool.Shared.Rent(0x100000); - byte[] test = ArrayPool.Shared.Rent(0x1000); + byte[] data = new byte[0x100000]; + byte[] test = new byte[0x1000]; using (MemoryStream ms = new MemoryStream(data)) { createData(ms); @@ -91,8 +88,5 @@ public class SharpCompressStreamTests Assert.Equal(0x10000, ms.Position); //the base stream has not moved } } - - ArrayPool.Shared.Return(data); - ArrayPool.Shared.Return(test); } } diff --git a/tests/SharpCompress.Test/Streams/ZLibBaseStreamAsyncTests.cs b/tests/SharpCompress.Test/Streams/ZLibBaseStreamAsyncTests.cs new file mode 100644 index 00000000..3512b8b7 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/ZLibBaseStreamAsyncTests.cs @@ -0,0 +1,112 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; +using AwesomeAssertions; +using SharpCompress.Compressors; +using SharpCompress.Compressors.Deflate; +using SharpCompress.IO; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class ZLibBaseStreamAsyncTests +{ + [Fact] + public async Task TestChunkedZlibCompressesEverythingAsync() + { + var plainData = new byte[] + { + 0xf7, + 0x1b, + 0xda, + 0x0f, + 0xb6, + 0x2b, + 0x3d, + 0x91, + 0xd7, + 0xe1, + 0xb5, + 0x11, + 0x34, + 0x5a, + 0x51, + 0x3f, + 0x8b, + 0xce, + 0x49, + 0xd2, + }; + var buf = new byte[plainData.Length * 2]; + + var plainStream1 = new MemoryStream(plainData); + var compressor1 = new DeflateStream(plainStream1, CompressionMode.Compress); + // This is enough to read the entire data + var realCompressedSize = await compressor1 + .ReadAsync(buf, 0, plainData.Length * 2) + .ConfigureAwait(false); + + var plainStream2 = new MemoryStream(plainData); + var compressor2 = new DeflateStream(plainStream2, CompressionMode.Compress); + var total = 0; + var r = -1; // Jumpstart + while (r != 0) + { + // Reading in chunks + r = await compressor2.ReadAsync(buf, 0, plainData.Length).ConfigureAwait(false); + total += r; + } + + Assert.Equal(total, realCompressedSize); + } + + [Fact] + public async Task Zlib_should_read_the_previously_written_message_async() + { + var message = new string('a', 131073); // 131073 causes the failure, but 131072 (-1) doesn't + var bytes = Encoding.ASCII.GetBytes(message); + + using var inputStream = new MemoryStream(bytes); + using var compressedStream = new MemoryStream(); + using var byteBufferStream = new BufferedStream(inputStream); // System.IO + await CompressAsync(byteBufferStream, compressedStream, compressionLevel: 1) + .ConfigureAwait(false); + compressedStream.Position = 0; + + using var decompressedStream = new MemoryStream(); + await DecompressAsync(compressedStream, decompressedStream).ConfigureAwait(false); + + byteBufferStream.Position = 0; + var result = Encoding.ASCII.GetString( + await GetBytesAsync(byteBufferStream).ConfigureAwait(false) + ); + result.Should().Be(message); + } + + private async Task CompressAsync(Stream input, Stream output, int compressionLevel) + { + using var zlibStream = new ZlibStream( + SharpCompressStream.Create(output, leaveOpen: true), + CompressionMode.Compress, + (CompressionLevel)compressionLevel + ); + zlibStream.FlushMode = FlushType.Sync; + await input.CopyToAsync(zlibStream).ConfigureAwait(false); + } + + private async Task DecompressAsync(Stream input, Stream output) + { + using var zlibStream = new ZlibStream( + SharpCompressStream.Create(input, leaveOpen: true), + CompressionMode.Decompress + ); + await zlibStream.CopyToAsync(output).ConfigureAwait(false); + } + + private async Task GetBytesAsync(BufferedStream stream) + { + var bytes = new byte[stream.Length]; + await stream.ReadAsync(bytes, 0, (int)stream.Length).ConfigureAwait(false); + return bytes; + } +} diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index 7d7ed430..0bc93d83 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -207,13 +207,10 @@ public class TarReaderAsyncTests : ReaderTests Assert.Throws(() => reader.MoveToNextEntry()); } -#if !NETFRAMEWORK +#if LINUX [Fact] public async Task Tar_GZip_With_Symlink_Entries_Async() { - var isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform( - System.Runtime.InteropServices.OSPlatform.Windows - ); using Stream stream = File.OpenRead( Path.Combine(TEST_ARCHIVES_PATH, "TarWithSymlink.tar.gz") ); @@ -232,38 +229,32 @@ public class TarReaderAsyncTests : ReaderTests Overwrite = true, WriteSymbolicLink = (sourcePath, targetPath) => { - if (!isWindows) + var link = new Mono.Unix.UnixSymbolicLinkInfo(sourcePath); + if (File.Exists(sourcePath)) { - var link = new Mono.Unix.UnixSymbolicLinkInfo(sourcePath); - if (File.Exists(sourcePath)) - { - link.Delete(); // equivalent to ln -s -f - } - link.CreateSymbolicLinkTo(targetPath); + link.Delete(); // equivalent to ln -s -f } + link.CreateSymbolicLinkTo(targetPath); }, } ); - if (!isWindows) + if (reader.Entry.LinkTarget != null) { - if (reader.Entry.LinkTarget != null) + var path = Path.Combine(SCRATCH_FILES_PATH, reader.Entry.Key.NotNull()); + var link = new Mono.Unix.UnixSymbolicLinkInfo(path); + if (link.HasContents) { - var path = Path.Combine(SCRATCH_FILES_PATH, reader.Entry.Key.NotNull()); - var link = new Mono.Unix.UnixSymbolicLinkInfo(path); - if (link.HasContents) - { - // need to convert the link to an absolute path for comparison - var target = reader.Entry.LinkTarget; - var realTarget = Path.GetFullPath( - Path.Combine($"{Path.GetDirectoryName(path)}", target) - ); + // need to convert the link to an absolute path for comparison + var target = reader.Entry.LinkTarget; + var realTarget = Path.GetFullPath( + Path.Combine($"{Path.GetDirectoryName(path)}", target) + ); - Assert.Equal(realTarget, link.GetContents().ToString()); - } - else - { - Assert.True(false, "Symlink has no target"); - } + Assert.Equal(realTarget, link.GetContents().ToString()); + } + else + { + Assert.True(false, "Symlink has no target"); } } } 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 diff --git a/tests/TestArchives/Archives/Arj.store.arj b/tests/TestArchives/Archives/Arj.store.arj new file mode 100644 index 00000000..b6fa515c Binary files /dev/null and b/tests/TestArchives/Archives/Arj.store.arj differ