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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 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"); } } }