From 2aabd8d0e170d8929f90ba4739366dde826590d8 Mon Sep 17 00:00:00 2001 From: Twan van Dongen Date: Tue, 28 Oct 2025 20:38:29 +0100 Subject: [PATCH 01/14] Initial implementation of the ARJ (Archived by Robert Jung) format, supporting 'store' for single file archives. Multi-file archives and all compression algorithms still require implementations. --- src/SharpCompress/Common/ArchiveType.cs | 1 + src/SharpCompress/Common/Arj/ArjEntry.cs | 48 +++++ src/SharpCompress/Common/Arj/ArjFilePart.cs | 52 ++++++ src/SharpCompress/Common/Arj/ArjVolume.cs | 16 ++ .../Common/Arj/Headers/ArjHeader.cs | 115 ++++++++++++ .../Common/Arj/Headers/ArjLocalHeader.cs | 175 ++++++++++++++++++ .../Common/Arj/Headers/ArjMainHeader.cs | 150 +++++++++++++++ .../Common/Arj/Headers/CompressionMethod.cs | 20 ++ .../Common/Arj/Headers/DosDateTime.cs | 37 ++++ .../Common/Arj/Headers/FileType.cs | 13 ++ .../Common/Arj/Headers/HostOS.cs | 19 ++ src/SharpCompress/Factories/ArjFactory.cs | 42 +++++ src/SharpCompress/Factories/Factory.cs | 1 + src/SharpCompress/Readers/Arj/ArjReader.cs | 50 +++++ src/SharpCompress/Readers/ReaderFactory.cs | 2 +- src/SharpCompress/packages.lock.json | 6 +- .../SharpCompress.Test/Arj/ArjReaderTests.cs | 46 +++++ tests/TestArchives/Archives/Arj.store.arj | Bin 0 -> 101138 bytes 18 files changed, 789 insertions(+), 4 deletions(-) create mode 100644 src/SharpCompress/Common/Arj/ArjEntry.cs create mode 100644 src/SharpCompress/Common/Arj/ArjFilePart.cs create mode 100644 src/SharpCompress/Common/Arj/ArjVolume.cs create mode 100644 src/SharpCompress/Common/Arj/Headers/ArjHeader.cs create mode 100644 src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs create mode 100644 src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs create mode 100644 src/SharpCompress/Common/Arj/Headers/CompressionMethod.cs create mode 100644 src/SharpCompress/Common/Arj/Headers/DosDateTime.cs create mode 100644 src/SharpCompress/Common/Arj/Headers/FileType.cs create mode 100644 src/SharpCompress/Common/Arj/Headers/HostOS.cs create mode 100644 src/SharpCompress/Factories/ArjFactory.cs create mode 100644 src/SharpCompress/Readers/Arj/ArjReader.cs create mode 100644 tests/SharpCompress.Test/Arj/ArjReaderTests.cs create mode 100644 tests/TestArchives/Archives/Arj.store.arj 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..2e83190c --- /dev/null +++ b/src/SharpCompress/Common/Arj/ArjEntry.cs @@ -0,0 +1,48 @@ +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 => CompressionType.None; + + 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 == (int)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..24dbbab5 --- /dev/null +++ b/src/SharpCompress/Common/Arj/ArjFilePart.cs @@ -0,0 +1,52 @@ +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.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; + 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..cb8fc479 --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/ArjHeader.cs @@ -0,0 +1,115 @@ +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 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); + } + } + } +} diff --git a/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs b/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs new file mode 100644 index 00000000..595037f5 --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs @@ -0,0 +1,175 @@ +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 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; } + 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 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) + { + 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++]; + byte archiverVersionNumber = headerBytes[offset++]; + byte minVersionToExtract = headerBytes[offset++]; + HostOS hostOS = (HostOS)headerBytes[offset++]; + byte arjFlags = headerBytes[offset++]; + CompressionMethod compressionMethod = CompressionMethodFromByte(headerBytes[offset++]); + FileType 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, + }; + } + + 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 new file mode 100644 index 00000000..f050837c --- /dev/null +++ b/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.cs @@ -0,0 +1,150 @@ +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 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; } + 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; + + int ReadByte() + { + if (offset >= headerBytes.Length) + { + throw new EndOfStreamException(); + } + return 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 = 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; + } + + // 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/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/Factories/ArjFactory.cs b/src/SharpCompress/Factories/ArjFactory.cs new file mode 100644 index 00000000..597393a6 --- /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.Arc; + +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..37bde917 --- /dev/null +++ b/src/SharpCompress/Readers/Arj/ArjReader.cs @@ -0,0 +1,50 @@ +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.Arc +{ + 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) + { + var headerReader = new ArjMainHeader(new ArchiveEncoding()); + var mainHeader = headerReader.Read(stream); + + while (true) + { + var localReader = new ArjLocalHeader(new ArchiveEncoding()); + var localHeader = localReader.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/packages.lock.json b/src/SharpCompress/packages.lock.json index 9ebb1677..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.17, )", - "resolved": "8.0.17", - "contentHash": "x5/y4l8AtshpBOrCZdlE4txw8K3e3s9meBFeZeR3l8hbbku2V7kK6ojhXvrbjg1rk3G+JqL1BI26gtgc1ZrdUw==" + "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..84a5e437 --- /dev/null +++ b/tests/SharpCompress.Test/Arj/ArjReaderTests.cs @@ -0,0 +1,46 @@ +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 SharpCompress.Readers.Arc; +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); + + 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(); + } + } +} diff --git a/tests/TestArchives/Archives/Arj.store.arj b/tests/TestArchives/Archives/Arj.store.arj new file mode 100644 index 0000000000000000000000000000000000000000..b6fa515c8282199092c6cc1199aa4d33f5103025 GIT binary patch literal 101138 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

&z165c$4I;MkjzUCQLVq{lwYpst&GI6; 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 literal 0 HcmV?d00001 From 321233b82c90338868009a4908940b5713822596 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 30 Oct 2025 15:08:57 +0000 Subject: [PATCH 02/14] some async implementations --- .../Compressors/LZMA/AesDecoderStream.cs | 67 +++++++ .../Compressors/LZMA/LzmaStream.cs | 185 ++++++++++++++++++ .../LZMA/Utilites/CrcBuilderStream.cs | 20 ++ 3 files changed, 272 insertions(+) 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/LzmaStream.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs index 4d80704e..34faf0a8 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,112 @@ public class LzmaStream : Stream, IStreamStack } } + public override async Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + 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 += _outWindow.CopyStream(_inputStream, toProcess); + } + else if (_decoder.Code(_dictionarySize, _outWindow, _rangeDecoder) && _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 (!_decoder.Code(_dictionarySize, _outWindow, _rangeDecoder)) + { + _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 async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + Write(buffer, offset, count); + await 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); + } } From d4f11e00b1ba0d9ab5fe802a97684da09015001e Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 30 Oct 2025 16:31:45 +0000 Subject: [PATCH 03/14] some more async changes --- .../Compressors/LZMA/Bcj2DecoderStream.cs | 14 ++++++++++ .../Compressors/LZMA/LZipStream.cs | 27 +++++++++++++++++++ .../LZMA/Utilites/CrcCheckStream.cs | 14 ++++++++++ 3 files changed, 55 insertions(+) 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/LZipStream.cs b/src/SharpCompress/Compressors/LZMA/LZipStream.cs index 26d34d3a..db0223dc 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; @@ -149,6 +151,7 @@ public sealed class LZipStream : Stream, IStreamStack public override int Read(byte[] buffer, int offset, int count) => _stream.Read(buffer, offset, count); + public override int ReadByte() => _stream.ReadByte(); public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); @@ -157,6 +160,7 @@ public sealed class LZipStream : Stream, IStreamStack #if !NETFRAMEWORK && !NETSTANDARD2_0 + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = new CancellationToken()) => _stream.ReadAsync(buffer, cancellationToken); public override int Read(Span buffer) => _stream.Read(buffer); public override void Write(ReadOnlySpan buffer) @@ -179,6 +183,29 @@ public sealed class LZipStream : Stream, IStreamStack ++_writeCount; } + public override Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return _stream.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + await _stream.WriteAsync(buffer, offset, count, cancellationToken); + _writeCount += count; + } + #endregion ///

diff --git a/src/SharpCompress/Compressors/LZMA/Utilites/CrcCheckStream.cs b/src/SharpCompress/Compressors/LZMA/Utilites/CrcCheckStream.cs index c10efda1..24a85ed8 100644 --- a/src/SharpCompress/Compressors/LZMA/Utilites/CrcCheckStream.cs +++ b/src/SharpCompress/Compressors/LZMA/Utilites/CrcCheckStream.cs @@ -1,6 +1,8 @@ using System; using System.Diagnostics; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.Compressors.LZMA.Utilites; @@ -101,4 +103,16 @@ public class CrcCheckStream : Stream _mCurrentCrc = Crc.Update(_mCurrentCrc, buffer, offset, count); } + + public override async Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + Write(buffer, offset, count); + await Task.CompletedTask; + } } From 53c96193c1aa1b150ad07531cf9237d75b400006 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 30 Oct 2025 16:40:59 +0000 Subject: [PATCH 04/14] format --- src/SharpCompress/Compressors/LZMA/LZipStream.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/SharpCompress/Compressors/LZMA/LZipStream.cs b/src/SharpCompress/Compressors/LZMA/LZipStream.cs index db0223dc..10a90c58 100644 --- a/src/SharpCompress/Compressors/LZMA/LZipStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LZipStream.cs @@ -151,7 +151,6 @@ public sealed class LZipStream : Stream, IStreamStack public override int Read(byte[] buffer, int offset, int count) => _stream.Read(buffer, offset, count); - public override int ReadByte() => _stream.ReadByte(); public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); @@ -160,7 +159,11 @@ public sealed class LZipStream : Stream, IStreamStack #if !NETFRAMEWORK && !NETSTANDARD2_0 - public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = new CancellationToken()) => _stream.ReadAsync(buffer, cancellationToken); + public override ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = new CancellationToken() + ) => _stream.ReadAsync(buffer, cancellationToken); + public override int Read(Span buffer) => _stream.Read(buffer); public override void Write(ReadOnlySpan buffer) @@ -183,7 +186,7 @@ public sealed class LZipStream : Stream, IStreamStack ++_writeCount; } - public override Task ReadAsync( + public override Task ReadAsync( byte[] buffer, int offset, int count, From 60e1dc023932307605e141a1782923e34449fa78 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 31 Oct 2025 11:10:55 +0000 Subject: [PATCH 05/14] review fixes --- src/SharpCompress/Compressors/LZMA/LZipStream.cs | 9 +++------ src/SharpCompress/Compressors/LZMA/LzmaStream.cs | 6 +++--- .../Compressors/LZMA/Utilites/CrcCheckStream.cs | 10 ++++++---- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/SharpCompress/Compressors/LZMA/LZipStream.cs b/src/SharpCompress/Compressors/LZMA/LZipStream.cs index 10a90c58..f9398ecf 100644 --- a/src/SharpCompress/Compressors/LZMA/LZipStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LZipStream.cs @@ -161,7 +161,7 @@ public sealed class LZipStream : Stream, IStreamStack public override ValueTask ReadAsync( Memory buffer, - CancellationToken cancellationToken = new CancellationToken() + CancellationToken cancellationToken = default ) => _stream.ReadAsync(buffer, cancellationToken); public override int Read(Span buffer) => _stream.Read(buffer); @@ -191,11 +191,8 @@ public sealed class LZipStream : Stream, IStreamStack int offset, int count, CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return _stream.ReadAsync(buffer, offset, count, cancellationToken); - } + ) => + _stream.ReadAsync(buffer, offset, count, cancellationToken); public override async Task WriteAsync( byte[] buffer, diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs index 34faf0a8..484ca46a 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs @@ -555,7 +555,7 @@ public class LzmaStream : Stream, IStreamStack _outWindow.SetLimit(toProcess); if (_uncompressedChunk) { - _inputPosition += _outWindow.CopyStream(_inputStream, toProcess); + _inputPosition += await _outWindow.CopyStreamAsync(_inputStream, toProcess, cancellationToken).ConfigureAwait(false); } else if (_decoder.Code(_dictionarySize, _outWindow, _rangeDecoder) && _outputSize < 0) { @@ -608,7 +608,7 @@ public class LzmaStream : Stream, IStreamStack return total; } - public override async Task WriteAsync( + public override Task WriteAsync( byte[] buffer, int offset, int count, @@ -617,7 +617,7 @@ public class LzmaStream : Stream, IStreamStack { cancellationToken.ThrowIfCancellationRequested(); Write(buffer, offset, count); - await Task.CompletedTask; + return Task.CompletedTask; } public byte[] Properties { get; } = new byte[5]; diff --git a/src/SharpCompress/Compressors/LZMA/Utilites/CrcCheckStream.cs b/src/SharpCompress/Compressors/LZMA/Utilites/CrcCheckStream.cs index 24a85ed8..c90ebfda 100644 --- a/src/SharpCompress/Compressors/LZMA/Utilites/CrcCheckStream.cs +++ b/src/SharpCompress/Compressors/LZMA/Utilites/CrcCheckStream.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Diagnostics; using System.IO; using System.Threading; @@ -13,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) @@ -67,6 +68,7 @@ public class CrcCheckStream : Stream finally { base.Dispose(disposing); + ArrayPool.Shared.Return(_mBytes); } } @@ -104,15 +106,15 @@ public class CrcCheckStream : Stream _mCurrentCrc = Crc.Update(_mCurrentCrc, buffer, offset, count); } - public override async Task WriteAsync( + public override Task WriteAsync( byte[] buffer, int offset, int count, - CancellationToken cancellationToken = default + CancellationToken cancellationToken ) { cancellationToken.ThrowIfCancellationRequested(); Write(buffer, offset, count); - await Task.CompletedTask; + return Task.CompletedTask; } } From e1e9c449e94a55661debb9db38dc9d623846c40f Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 31 Oct 2025 11:12:46 +0000 Subject: [PATCH 06/14] use proper async versions --- .../Compressors/LZMA/LZ/LzOutWindow.cs | 61 +++++++++++++++++++ .../Compressors/LZMA/LzmaStream.cs | 6 +- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs index 6b9c525f..ade6009b 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; @@ -104,6 +106,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) @@ -207,6 +230,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/LzmaStream.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs index 484ca46a..5e88fd7d 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs @@ -517,7 +517,7 @@ public class LzmaStream : Stream, IStreamStack byte[] buffer, int offset, int count, - CancellationToken cancellationToken = default + CancellationToken cancellationToken ) { if (_endReached) @@ -555,7 +555,9 @@ public class LzmaStream : Stream, IStreamStack _outWindow.SetLimit(toProcess); if (_uncompressedChunk) { - _inputPosition += await _outWindow.CopyStreamAsync(_inputStream, toProcess, cancellationToken).ConfigureAwait(false); + _inputPosition += await _outWindow + .CopyStreamAsync(_inputStream, toProcess, cancellationToken) + .ConfigureAwait(false); } else if (_decoder.Code(_dictionarySize, _outWindow, _rangeDecoder) && _outputSize < 0) { From 74e2dca207cdec823ef95ccff6a56fbddda03bde Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 31 Oct 2025 11:15:42 +0000 Subject: [PATCH 07/14] fmt --- src/SharpCompress/Compressors/LZMA/LZipStream.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/SharpCompress/Compressors/LZMA/LZipStream.cs b/src/SharpCompress/Compressors/LZMA/LZipStream.cs index f9398ecf..9b6eef57 100644 --- a/src/SharpCompress/Compressors/LZMA/LZipStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LZipStream.cs @@ -191,8 +191,7 @@ public sealed class LZipStream : Stream, IStreamStack int offset, int count, CancellationToken cancellationToken = default - ) => - _stream.ReadAsync(buffer, offset, count, cancellationToken); + ) => _stream.ReadAsync(buffer, offset, count, cancellationToken); public override async Task WriteAsync( byte[] buffer, From 1e22b47fe1f9ef71f64ff5dc62123f67a49132d2 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 31 Oct 2025 11:23:30 +0000 Subject: [PATCH 08/14] some fixes --- .../Compressors/LZMA/LZipStream.cs | 2 +- .../Compressors/LZMA/LzmaDecoder.cs | 29 +++---------------- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/SharpCompress/Compressors/LZMA/LZipStream.cs b/src/SharpCompress/Compressors/LZMA/LZipStream.cs index 9b6eef57..e7059003 100644 --- a/src/SharpCompress/Compressors/LZMA/LZipStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LZipStream.cs @@ -197,7 +197,7 @@ public sealed class LZipStream : Stream, IStreamStack byte[] buffer, int offset, int count, - CancellationToken cancellationToken = default + CancellationToken cancellationToken ) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs index 4baa9061..1de9dc2f 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) @@ -470,29 +474,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) {} - */ } From c53ca372f2f28a7bb562afcdafa68aa0805be8b8 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 31 Oct 2025 11:39:57 +0000 Subject: [PATCH 09/14] don't use pools in tests --- .../Streams/LzmaStreamTests.cs | 3 +- .../Streams/SharpCompressStreamAsyncTests.cs | 169 +++++++----------- .../Streams/SharpCompressStreamTest.cs | 14 +- 3 files changed, 72 insertions(+), 114 deletions(-) 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/SharpCompressStreamAsyncTests.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs index a33560a6..bca1ad5f 100644 --- a/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs @@ -28,148 +28,113 @@ 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); + IStreamStack stack = (IStreamStack)scs; - using (SharpCompressStream scs = new SharpCompressStream(ms, true, false, 0x10000)) - { - IStreamStack stack = (IStreamStack)scs; + scs.Seek(0x1000, SeekOrigin.Begin); + Assert.Equal(0x1000, scs.Position); // position in the SharpCompressionStream + Assert.Equal(0x1000, ms.Position); // initial seek + full buffer read - 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 - 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); } } From 7fa271a1b41a99f178b06db5ce2ccf0d2a4377b5 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 31 Oct 2025 11:43:35 +0000 Subject: [PATCH 10/14] fix ILLink --- Directory.Packages.props | 1 + src/SharpCompress/SharpCompress.csproj | 3 +++ src/SharpCompress/packages.lock.json | 6 +++--- 3 files changed, 7 insertions(+), 3 deletions(-) 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/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", From 55cb350d2c54caa9bb2e4fceaf9c101219f8f2d4 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Fri, 31 Oct 2025 11:44:32 +0000 Subject: [PATCH 11/14] remove needless variable --- .../SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs b/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs index bca1ad5f..d806246d 100644 --- a/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs +++ b/tests/SharpCompress.Test/Streams/SharpCompressStreamAsyncTests.cs @@ -36,8 +36,6 @@ public class SharpCompressStreamAsyncTests using (SharpCompressStream scs = new SharpCompressStream(ms, true, false, 0x10000)) { - IStreamStack stack = (IStreamStack)scs; - scs.Seek(0x1000, SeekOrigin.Begin); Assert.Equal(0x1000, scs.Position); // position in the SharpCompressionStream Assert.Equal(0x1000, ms.Position); // initial seek + full buffer read From f391c3caf37a27b270fee8edb53c61cc4ebc64a1 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 1 Nov 2025 09:42:17 +0000 Subject: [PATCH 12/14] make an async code --- .../Compressors/LZMA/LZ/LzOutWindow.cs | 69 +++++++ .../Compressors/LZMA/LzmaDecoder.cs | 171 ++++++++++++++++++ .../Compressors/LZMA/LzmaStream.cs | 6 +- 3 files changed, 243 insertions(+), 3 deletions(-) diff --git a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs index ade6009b..cb453851 100644 --- a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs +++ b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs @@ -87,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) @@ -147,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; @@ -180,6 +206,39 @@ 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; @@ -190,6 +249,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; diff --git a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs index 1de9dc2f..b2275841 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs @@ -313,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); @@ -439,6 +475,141 @@ 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) diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs index 5e88fd7d..6fb7afa1 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs @@ -559,7 +559,7 @@ public class LzmaStream : Stream, IStreamStack .CopyStreamAsync(_inputStream, toProcess, cancellationToken) .ConfigureAwait(false); } - else if (_decoder.Code(_dictionarySize, _outWindow, _rangeDecoder) && _outputSize < 0) + else if (await _decoder.CodeAsync(_dictionarySize, _outWindow, _rangeDecoder, cancellationToken).ConfigureAwait(false) && _outputSize < 0) { _availableBytes = _outWindow.AvailableBytes; } @@ -578,7 +578,7 @@ public class LzmaStream : Stream, IStreamStack ) { _outWindow.SetLimit(toProcess + 1); - if (!_decoder.Code(_dictionarySize, _outWindow, _rangeDecoder)) + if (!await _decoder.CodeAsync(_dictionarySize, _outWindow, _rangeDecoder, cancellationToken).ConfigureAwait(false)) { _rangeDecoder.ReleaseStream(); throw new DataErrorException(); @@ -614,7 +614,7 @@ public class LzmaStream : Stream, IStreamStack byte[] buffer, int offset, int count, - CancellationToken cancellationToken = default + CancellationToken cancellationToken ) { cancellationToken.ThrowIfCancellationRequested(); From 2d41de6b7245edb4b8886a19d74ce561d3712c9f Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 1 Nov 2025 09:57:26 +0000 Subject: [PATCH 13/14] add async tests --- .../Streams/LzmaStreamAsyncTests.cs | 606 ++++++++++++++++++ .../Streams/RewindableStreamAsyncTest.cs | 96 +++ .../Streams/ZLibBaseStreamAsyncTests.cs | 112 ++++ 3 files changed, 814 insertions(+) create mode 100644 tests/SharpCompress.Test/Streams/LzmaStreamAsyncTests.cs create mode 100644 tests/SharpCompress.Test/Streams/RewindableStreamAsyncTest.cs create mode 100644 tests/SharpCompress.Test/Streams/ZLibBaseStreamAsyncTests.cs 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/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/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; + } +} From f7b3525c4e349ab87330fd493bca32b952e88c7f Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 1 Nov 2025 10:25:14 +0000 Subject: [PATCH 14/14] fix tests and fmt --- .../Compressors/LZMA/LZ/LzOutWindow.cs | 6 ++- .../Compressors/LZMA/LzmaDecoder.cs | 4 +- .../Compressors/LZMA/LzmaStream.cs | 18 ++++++- .../Tar/TarReaderAsyncTests.cs | 47 ++++++++----------- 4 files changed, 43 insertions(+), 32 deletions(-) diff --git a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs index cb453851..c65cc683 100644 --- a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs +++ b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs @@ -206,7 +206,11 @@ internal class OutWindow : IDisposable _pendingDist = distance; } - public async Task CopyBlockAsync(int distance, int len, CancellationToken cancellationToken = default) + public async Task CopyBlockAsync( + int distance, + int len, + CancellationToken cancellationToken = default + ) { var rem = len; var pos = (distance < _pos ? _pos : _pos + _windowSize) - distance - 1; diff --git a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs index b2275841..fed40533 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs @@ -604,7 +604,9 @@ public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream } throw new DataErrorException(); } - await outWindow.CopyBlockAsync((int)_rep0, (int)len, cancellationToken).ConfigureAwait(false); + await outWindow + .CopyBlockAsync((int)_rep0, (int)len, cancellationToken) + .ConfigureAwait(false); } } return false; diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs index 6fb7afa1..eaef5fd3 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs @@ -559,7 +559,12 @@ public class LzmaStream : Stream, IStreamStack .CopyStreamAsync(_inputStream, toProcess, cancellationToken) .ConfigureAwait(false); } - else if (await _decoder.CodeAsync(_dictionarySize, _outWindow, _rangeDecoder, cancellationToken).ConfigureAwait(false) && _outputSize < 0) + else if ( + await _decoder + .CodeAsync(_dictionarySize, _outWindow, _rangeDecoder, cancellationToken) + .ConfigureAwait(false) + && _outputSize < 0 + ) { _availableBytes = _outWindow.AvailableBytes; } @@ -578,7 +583,16 @@ public class LzmaStream : Stream, IStreamStack ) { _outWindow.SetLimit(toProcess + 1); - if (!await _decoder.CodeAsync(_dictionarySize, _outWindow, _rangeDecoder, cancellationToken).ConfigureAwait(false)) + if ( + !await _decoder + .CodeAsync( + _dictionarySize, + _outWindow, + _rangeDecoder, + cancellationToken + ) + .ConfigureAwait(false) + ) { _rangeDecoder.ReleaseStream(); throw new DataErrorException(); 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"); } } }