diff --git a/src/SharpCompress/Common/Arc/ArcEntry.cs b/src/SharpCompress/Common/Arc/ArcEntry.cs index a67f10d1..f34e30c0 100644 --- a/src/SharpCompress/Common/Arc/ArcEntry.cs +++ b/src/SharpCompress/Common/Arc/ArcEntry.cs @@ -7,54 +7,53 @@ using System.Threading.Tasks; using SharpCompress.Common.GZip; using SharpCompress.Common.Tar; -namespace SharpCompress.Common.Arc +namespace SharpCompress.Common.Arc; + +public class ArcEntry : Entry { - public class ArcEntry : Entry + private readonly ArcFilePart? _filePart; + + internal ArcEntry(ArcFilePart? filePart) { - private readonly ArcFilePart? _filePart; - - internal ArcEntry(ArcFilePart? filePart) - { - _filePart = filePart; - } - - public override long Crc - { - get - { - if (_filePart == null) - { - return 0; - } - return _filePart.Header.Crc16; - } - } - - public override string? Key => _filePart?.Header.Name; - - public override string? LinkTarget => null; - - public override long CompressedSize => _filePart?.Header.CompressedSize ?? 0; - - public override CompressionType CompressionType => - _filePart?.Header.CompressionMethod ?? CompressionType.Unknown; - - public override long Size => throw new NotImplementedException(); - - public override DateTime? LastModifiedTime => null; - - public override DateTime? CreatedTime => null; - - public override DateTime? LastAccessedTime => null; - - public override DateTime? ArchivedTime => null; - - public override bool IsEncrypted => false; - - public override bool IsDirectory => false; - - public override bool IsSplitAfter => false; - - internal override IEnumerable Parts => _filePart.Empty(); + _filePart = filePart; } -} + + public override long Crc + { + get + { + if (_filePart == null) + { + return 0; + } + return _filePart.Header.Crc16; + } + } + + public override string? Key => _filePart?.Header.Name; + + public override string? LinkTarget => null; + + public override long CompressedSize => _filePart?.Header.CompressedSize ?? 0; + + public override CompressionType CompressionType => + _filePart?.Header.CompressionMethod ?? CompressionType.Unknown; + + public override long Size => throw new NotImplementedException(); + + public override DateTime? LastModifiedTime => null; + + public override DateTime? CreatedTime => null; + + public override DateTime? LastAccessedTime => null; + + public override DateTime? ArchivedTime => null; + + public override bool IsEncrypted => false; + + public override bool IsDirectory => false; + + public override bool IsSplitAfter => false; + + internal override IEnumerable Parts => _filePart.Empty(); +} \ No newline at end of file diff --git a/src/SharpCompress/Common/Arc/ArcEntryHeader.cs b/src/SharpCompress/Common/Arc/ArcEntryHeader.cs index 137b190a..e7ea1c03 100644 --- a/src/SharpCompress/Common/Arc/ArcEntryHeader.cs +++ b/src/SharpCompress/Common/Arc/ArcEntryHeader.cs @@ -3,74 +3,73 @@ using System.IO; using System.Linq; using System.Text; -namespace SharpCompress.Common.Arc +namespace SharpCompress.Common.Arc; + +public class ArcEntryHeader { - public class ArcEntryHeader + public ArchiveEncoding ArchiveEncoding { get; } + public CompressionType CompressionMethod { get; private set; } + public string? Name { get; private set; } + public long CompressedSize { get; private set; } + public DateTime DateTime { get; private set; } + public int Crc16 { get; private set; } + public long OriginalSize { get; private set; } + public long DataStartPosition { get; private set; } + + public ArcEntryHeader(ArchiveEncoding archiveEncoding) { - public ArchiveEncoding ArchiveEncoding { get; } - public CompressionType CompressionMethod { get; private set; } - public string? Name { get; private set; } - public long CompressedSize { get; private set; } - public DateTime DateTime { get; private set; } - public int Crc16 { get; private set; } - public long OriginalSize { get; private set; } - public long DataStartPosition { get; private set; } - - public ArcEntryHeader(ArchiveEncoding archiveEncoding) - { - this.ArchiveEncoding = archiveEncoding; - } - - public ArcEntryHeader? ReadHeader(Stream stream) - { - byte[] headerBytes = new byte[29]; - if (stream.Read(headerBytes, 0, headerBytes.Length) != headerBytes.Length) - { - return null; - } - DataStartPosition = stream.Position; - return LoadFrom(headerBytes); - } - - public ArcEntryHeader LoadFrom(byte[] headerBytes) - { - CompressionMethod = GetCompressionType(headerBytes[1]); - - // Read name - int nameEnd = Array.IndexOf(headerBytes, (byte)0, 1); // Find null terminator - Name = Encoding.UTF8.GetString(headerBytes, 2, nameEnd > 0 ? nameEnd - 2 : 12); - - int offset = 15; - CompressedSize = BitConverter.ToUInt32(headerBytes, offset); - offset += 4; - uint rawDateTime = BitConverter.ToUInt32(headerBytes, offset); - DateTime = ConvertToDateTime(rawDateTime); - offset += 4; - Crc16 = BitConverter.ToUInt16(headerBytes, offset); - offset += 2; - OriginalSize = BitConverter.ToUInt32(headerBytes, offset); - return this; - } - - private CompressionType GetCompressionType(byte value) - { - return value switch - { - 1 or 2 => CompressionType.None, - 3 => CompressionType.RLE90, - 4 => CompressionType.Squeezed, - 5 or 6 or 7 or 8 => CompressionType.Crunched, - 9 => CompressionType.Squashed, - 10 => CompressionType.Crushed, - 11 => CompressionType.Distilled, - _ => CompressionType.Unknown, - }; - } - - public static DateTime ConvertToDateTime(long rawDateTime) - { - // Convert Unix timestamp to DateTime (UTC) - return DateTimeOffset.FromUnixTimeSeconds(rawDateTime).UtcDateTime; - } + this.ArchiveEncoding = archiveEncoding; } -} + + public ArcEntryHeader? ReadHeader(Stream stream) + { + byte[] headerBytes = new byte[29]; + if (stream.Read(headerBytes, 0, headerBytes.Length) != headerBytes.Length) + { + return null; + } + DataStartPosition = stream.Position; + return LoadFrom(headerBytes); + } + + public ArcEntryHeader LoadFrom(byte[] headerBytes) + { + CompressionMethod = GetCompressionType(headerBytes[1]); + + // Read name + int nameEnd = Array.IndexOf(headerBytes, (byte)0, 1); // Find null terminator + Name = Encoding.UTF8.GetString(headerBytes, 2, nameEnd > 0 ? nameEnd - 2 : 12); + + int offset = 15; + CompressedSize = BitConverter.ToUInt32(headerBytes, offset); + offset += 4; + uint rawDateTime = BitConverter.ToUInt32(headerBytes, offset); + DateTime = ConvertToDateTime(rawDateTime); + offset += 4; + Crc16 = BitConverter.ToUInt16(headerBytes, offset); + offset += 2; + OriginalSize = BitConverter.ToUInt32(headerBytes, offset); + return this; + } + + private CompressionType GetCompressionType(byte value) + { + return value switch + { + 1 or 2 => CompressionType.None, + 3 => CompressionType.RLE90, + 4 => CompressionType.Squeezed, + 5 or 6 or 7 or 8 => CompressionType.Crunched, + 9 => CompressionType.Squashed, + 10 => CompressionType.Crushed, + 11 => CompressionType.Distilled, + _ => CompressionType.Unknown, + }; + } + + public static DateTime ConvertToDateTime(long rawDateTime) + { + // Convert Unix timestamp to DateTime (UTC) + return DateTimeOffset.FromUnixTimeSeconds(rawDateTime).UtcDateTime; + } +} \ No newline at end of file diff --git a/src/SharpCompress/Common/Arc/ArcFilePart.cs b/src/SharpCompress/Common/Arc/ArcFilePart.cs index d1ff2cfc..23173385 100644 --- a/src/SharpCompress/Common/Arc/ArcFilePart.cs +++ b/src/SharpCompress/Common/Arc/ArcFilePart.cs @@ -13,63 +13,62 @@ using SharpCompress.Compressors.RLE90; using SharpCompress.Compressors.Squeezed; using SharpCompress.IO; -namespace SharpCompress.Common.Arc +namespace SharpCompress.Common.Arc; + +public class ArcFilePart : FilePart { - public class ArcFilePart : FilePart + private readonly Stream? _stream; + + internal ArcFilePart(ArcEntryHeader localArcHeader, Stream? seekableStream) + : base(localArcHeader.ArchiveEncoding) { - private readonly Stream? _stream; - - internal ArcFilePart(ArcEntryHeader localArcHeader, Stream? seekableStream) - : base(localArcHeader.ArchiveEncoding) - { - _stream = seekableStream; - Header = localArcHeader; - } - - internal ArcEntryHeader Header { get; set; } - - internal override string? FilePartName => Header.Name; - - internal override Stream GetCompressedStream() - { - if (_stream != null) - { - Stream compressedStream; - switch (Header.CompressionMethod) - { - case CompressionType.None: - compressedStream = new ReadOnlySubStream( - _stream, - Header.DataStartPosition, - Header.CompressedSize - ); - break; - case CompressionType.RLE90: - compressedStream = new RunLength90Stream( - _stream, - (int)Header.CompressedSize - ); - break; - case CompressionType.Squeezed: - compressedStream = new SqueezeStream(_stream, (int)Header.CompressedSize); - break; - case CompressionType.Crunched: - compressedStream = new ArcLzwStream( - _stream, - (int)Header.CompressedSize, - true - ); - break; - default: - throw new NotSupportedException( - "CompressionMethod: " + Header.CompressionMethod - ); - } - return compressedStream; - } - return _stream.NotNull(); - } - - internal override Stream? GetRawStream() => _stream; + _stream = seekableStream; + Header = localArcHeader; } -} + + internal ArcEntryHeader Header { get; set; } + + internal override string? FilePartName => Header.Name; + + internal override Stream GetCompressedStream() + { + if (_stream != null) + { + Stream compressedStream; + switch (Header.CompressionMethod) + { + case CompressionType.None: + compressedStream = new ReadOnlySubStream( + _stream, + Header.DataStartPosition, + Header.CompressedSize + ); + break; + case CompressionType.RLE90: + compressedStream = new RunLength90Stream( + _stream, + (int)Header.CompressedSize + ); + break; + case CompressionType.Squeezed: + compressedStream = new SqueezeStream(_stream, (int)Header.CompressedSize); + break; + case CompressionType.Crunched: + compressedStream = new ArcLzwStream( + _stream, + (int)Header.CompressedSize, + true + ); + break; + default: + throw new NotSupportedException( + "CompressionMethod: " + Header.CompressionMethod + ); + } + return compressedStream; + } + return _stream.NotNull(); + } + + internal override Stream? GetRawStream() => _stream; +} \ No newline at end of file diff --git a/src/SharpCompress/Common/Arc/ArcVolume.cs b/src/SharpCompress/Common/Arc/ArcVolume.cs index 8ebd11ea..83884185 100644 --- a/src/SharpCompress/Common/Arc/ArcVolume.cs +++ b/src/SharpCompress/Common/Arc/ArcVolume.cs @@ -6,11 +6,10 @@ using System.Text; using System.Threading.Tasks; using SharpCompress.Readers; -namespace SharpCompress.Common.Arc +namespace SharpCompress.Common.Arc; + +public class ArcVolume : Volume { - public class ArcVolume : Volume - { - public ArcVolume(Stream stream, ReaderOptions readerOptions, int index = 0) - : base(stream, readerOptions, index) { } - } -} + public ArcVolume(Stream stream, ReaderOptions readerOptions, int index = 0) + : base(stream, readerOptions, index) { } +} \ No newline at end of file diff --git a/src/SharpCompress/Compressors/Filters/DeltaFilter.cs b/src/SharpCompress/Compressors/Filters/DeltaFilter.cs index a6954116..2f1c2b2e 100644 --- a/src/SharpCompress/Compressors/Filters/DeltaFilter.cs +++ b/src/SharpCompress/Compressors/Filters/DeltaFilter.cs @@ -1,36 +1,35 @@ using System.IO; -namespace SharpCompress.Compressors.Filters +namespace SharpCompress.Compressors.Filters; + +internal class DeltaFilter : Filter { - internal class DeltaFilter : Filter + private const int DISTANCE_MIN = 1; + private const int DISTANCE_MAX = 256; + private const int DISTANCE_MASK = DISTANCE_MAX - 1; + + private int _distance; + private byte[] _history; + private int _position; + + public DeltaFilter(bool isEncoder, Stream baseStream, byte[] info) + : base(isEncoder, baseStream, 1) { - private const int DISTANCE_MIN = 1; - private const int DISTANCE_MAX = 256; - private const int DISTANCE_MASK = DISTANCE_MAX - 1; - - private int _distance; - private byte[] _history; - private int _position; - - public DeltaFilter(bool isEncoder, Stream baseStream, byte[] info) - : base(isEncoder, baseStream, 1) - { - _distance = info[0]; - _history = new byte[DISTANCE_MAX]; - _position = 0; - } - - protected override int Transform(byte[] buffer, int offset, int count) - { - var end = offset + count; - - for (var i = offset; i < end; i++) - { - buffer[i] += _history[(_distance + _position--) & DISTANCE_MASK]; - _history[_position & DISTANCE_MASK] = buffer[i]; - } - - return count; - } + _distance = info[0]; + _history = new byte[DISTANCE_MAX]; + _position = 0; } -} + + protected override int Transform(byte[] buffer, int offset, int count) + { + var end = offset + count; + + for (var i = offset; i < end; i++) + { + buffer[i] += _history[(_distance + _position--) & DISTANCE_MASK]; + _history[_position & DISTANCE_MASK] = buffer[i]; + } + + return count; + } +} \ No newline at end of file diff --git a/src/SharpCompress/Compressors/Lzw/LzwConstants.cs b/src/SharpCompress/Compressors/Lzw/LzwConstants.cs index 0325adbb..7e8a6399 100644 --- a/src/SharpCompress/Compressors/Lzw/LzwConstants.cs +++ b/src/SharpCompress/Compressors/Lzw/LzwConstants.cs @@ -1,65 +1,64 @@ -namespace SharpCompress.Compressors.Lzw +namespace SharpCompress.Compressors.Lzw; + +/// +/// This class contains constants used for LZW +/// +[System.Diagnostics.CodeAnalysis.SuppressMessage( + "Naming", + "CA1707:Identifiers should not contain underscores", + Justification = "kept for backwards compatibility" +)] +public sealed class LzwConstants { /// - /// This class contains constants used for LZW + /// Magic number found at start of LZW header: 0x1f 0x9d /// - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Naming", - "CA1707:Identifiers should not contain underscores", - Justification = "kept for backwards compatibility" - )] - public sealed class LzwConstants - { - /// - /// Magic number found at start of LZW header: 0x1f 0x9d - /// - public const int MAGIC = 0x1f9d; + public const int MAGIC = 0x1f9d; - /// - /// Maximum number of bits per code - /// - public const int MAX_BITS = 16; + /// + /// Maximum number of bits per code + /// + public const int MAX_BITS = 16; - /* 3rd header byte: - * bit 0..4 Number of compression bits - * bit 5 Extended header - * bit 6 Free - * bit 7 Block mode - */ + /* 3rd header byte: + * bit 0..4 Number of compression bits + * bit 5 Extended header + * bit 6 Free + * bit 7 Block mode + */ - /// - /// Mask for 'number of compression bits' - /// - public const int BIT_MASK = 0x1f; + /// + /// Mask for 'number of compression bits' + /// + public const int BIT_MASK = 0x1f; - /// - /// Indicates the presence of a fourth header byte - /// - public const int EXTENDED_MASK = 0x20; + /// + /// Indicates the presence of a fourth header byte + /// + public const int EXTENDED_MASK = 0x20; - //public const int FREE_MASK = 0x40; + //public const int FREE_MASK = 0x40; - /// - /// Reserved bits - /// - public const int RESERVED_MASK = 0x60; + /// + /// Reserved bits + /// + public const int RESERVED_MASK = 0x60; - /// - /// Block compression: if table is full and compression rate is dropping, - /// clear the dictionary. - /// - public const int BLOCK_MODE_MASK = 0x80; + /// + /// Block compression: if table is full and compression rate is dropping, + /// clear the dictionary. + /// + public const int BLOCK_MODE_MASK = 0x80; - /// - /// LZW file header size (in bytes) - /// - public const int HDR_SIZE = 3; + /// + /// LZW file header size (in bytes) + /// + public const int HDR_SIZE = 3; - /// - /// Initial number of bits per code - /// - public const int INIT_BITS = 9; + /// + /// Initial number of bits per code + /// + public const int INIT_BITS = 9; - private LzwConstants() { } - } -} + private LzwConstants() { } +} \ No newline at end of file diff --git a/src/SharpCompress/Compressors/Lzw/LzwStream.cs b/src/SharpCompress/Compressors/Lzw/LzwStream.cs index 71132dac..9642b40b 100644 --- a/src/SharpCompress/Compressors/Lzw/LzwStream.cs +++ b/src/SharpCompress/Compressors/Lzw/LzwStream.cs @@ -3,402 +3,75 @@ using System.IO; using SharpCompress.Common; using SharpCompress.IO; -namespace SharpCompress.Compressors.Lzw +namespace SharpCompress.Compressors.Lzw; + +/// +/// This filter stream is used to decompress a LZW format stream. +/// Specifically, a stream that uses the LZC compression method. +/// This file format is usually associated with the .Z file extension. +/// +/// See http://en.wikipedia.org/wiki/Compress +/// See http://wiki.wxwidgets.org/Development:_Z_File_Format +/// +/// The file header consists of 3 (or optionally 4) bytes. The first two bytes +/// contain the magic marker "0x1f 0x9d", followed by a byte of flags. +/// +/// Based on Java code by Ronald Tschalar, which in turn was based on the unlzw.c +/// code in the gzip package. +/// +/// This sample shows how to unzip a compressed file +/// +/// using System; +/// using System.IO; +/// +/// using ICSharpCode.SharpZipLib.Core; +/// using ICSharpCode.SharpZipLib.LZW; +/// +/// class MainClass +/// { +/// public static void Main(string[] args) +/// { +/// using (Stream inStream = new LzwInputStream(File.OpenRead(args[0]))) +/// using (FileStream outStream = File.Create(Path.GetFileNameWithoutExtension(args[0]))) { +/// byte[] buffer = new byte[4096]; +/// StreamUtils.Copy(inStream, outStream, buffer); +/// // OR +/// inStream.Read(buffer, 0, buffer.Length); +/// // now do something with the buffer +/// } +/// } +/// } +/// +/// +public class LzwStream : Stream, IStreamStack { - /// - /// This filter stream is used to decompress a LZW format stream. - /// Specifically, a stream that uses the LZC compression method. - /// This file format is usually associated with the .Z file extension. - /// - /// See http://en.wikipedia.org/wiki/Compress - /// See http://wiki.wxwidgets.org/Development:_Z_File_Format - /// - /// The file header consists of 3 (or optionally 4) bytes. The first two bytes - /// contain the magic marker "0x1f 0x9d", followed by a byte of flags. - /// - /// Based on Java code by Ronald Tschalar, which in turn was based on the unlzw.c - /// code in the gzip package. - /// - /// This sample shows how to unzip a compressed file - /// - /// using System; - /// using System.IO; - /// - /// using ICSharpCode.SharpZipLib.Core; - /// using ICSharpCode.SharpZipLib.LZW; - /// - /// class MainClass - /// { - /// public static void Main(string[] args) - /// { - /// using (Stream inStream = new LzwInputStream(File.OpenRead(args[0]))) - /// using (FileStream outStream = File.Create(Path.GetFileNameWithoutExtension(args[0]))) { - /// byte[] buffer = new byte[4096]; - /// StreamUtils.Copy(inStream, outStream, buffer); - /// // OR - /// inStream.Read(buffer, 0, buffer.Length); - /// // now do something with the buffer - /// } - /// } - /// } - /// - /// - public class LzwStream : Stream, IStreamStack - { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } #endif - int IStreamStack.DefaultBufferSize { get; set; } + int IStreamStack.DefaultBufferSize { get; set; } - Stream IStreamStack.BaseStream() => baseInputStream; + Stream IStreamStack.BaseStream() => baseInputStream; - int IStreamStack.BufferSize + int IStreamStack.BufferSize + { + get => 0; + set { } + } + int IStreamStack.BufferPosition + { + get => 0; + set { } + } + + void IStreamStack.SetPosition(long position) { } + + public static bool IsLzwStream(Stream stream) + { + try { - get => 0; - set { } - } - int IStreamStack.BufferPosition - { - get => 0; - set { } - } - - void IStreamStack.SetPosition(long position) { } - - public static bool IsLzwStream(Stream stream) - { - try - { - byte[] hdr = new byte[LzwConstants.HDR_SIZE]; - - int result = stream.Read(hdr, 0, hdr.Length); - - // Check the magic marker - if (result < 0) - throw new IncompleteArchiveException("Failed to read LZW header"); - - if (hdr[0] != (LzwConstants.MAGIC >> 8) || hdr[1] != (LzwConstants.MAGIC & 0xff)) - { - throw new IncompleteArchiveException( - String.Format( - "Wrong LZW header. Magic bytes don't match. 0x{0:x2} 0x{1:x2}", - hdr[0], - hdr[1] - ) - ); - } - } - catch (Exception) - { - return false; - } - return true; - } - - /// - /// Gets or sets a flag indicating ownership of underlying stream. - /// When the flag is true will close the underlying stream also. - /// - /// The default value is true. - public bool IsStreamOwner { get; set; } = false; - - /// - /// Creates a LzwInputStream - /// - /// - /// The stream to read compressed data from (baseInputStream LZW format) - /// - public LzwStream(Stream baseInputStream) - { - this.baseInputStream = baseInputStream; -#if DEBUG_STREAMS - this.DebugConstruct(typeof(LzwStream)); -#endif - } - - /// - /// See - /// - /// - public override int ReadByte() - { - int b = Read(one, 0, 1); - if (b == 1) - return (one[0] & 0xff); - return -1; - } - - /// - /// Reads decompressed data into the provided buffer byte array - /// - /// - /// The array to read and decompress data into - /// - /// - /// The offset indicating where the data should be placed - /// - /// - /// The number of bytes to decompress - /// - /// The number of bytes read. Zero signals the end of stream - public override int Read(byte[] buffer, int offset, int count) - { - if (!headerParsed) - ParseHeader(); - - if (eof) - return 0; - - int start = offset; - - /* Using local copies of various variables speeds things up by as - * much as 30% in Java! Performance not tested in C#. - */ - int[] lTabPrefix = tabPrefix; - byte[] lTabSuffix = tabSuffix; - byte[] lStack = stack; - int lNBits = nBits; - int lMaxCode = maxCode; - int lMaxMaxCode = maxMaxCode; - int lBitMask = bitMask; - int lOldCode = oldCode; - byte lFinChar = finChar; - int lStackP = stackP; - int lFreeEnt = freeEnt; - byte[] lData = data; - int lBitPos = bitPos; - - // empty stack if stuff still left - int sSize = lStack.Length - lStackP; - if (sSize > 0) - { - int num = (sSize >= count) ? count : sSize; - Array.Copy(lStack, lStackP, buffer, offset, num); - offset += num; - count -= num; - lStackP += num; - } - - if (count == 0) - { - stackP = lStackP; - return offset - start; - } - - // loop, filling local buffer until enough data has been decompressed - MainLoop: - do - { - if (end < EXTRA) - { - Fill(); - } - - int bitIn = (got > 0) ? (end - end % lNBits) << 3 : (end << 3) - (lNBits - 1); - - while (lBitPos < bitIn) - { - #region A - - // handle 1-byte reads correctly - if (count == 0) - { - nBits = lNBits; - maxCode = lMaxCode; - maxMaxCode = lMaxMaxCode; - bitMask = lBitMask; - oldCode = lOldCode; - finChar = lFinChar; - stackP = lStackP; - freeEnt = lFreeEnt; - bitPos = lBitPos; - - return offset - start; - } - - // check for code-width expansion - if (lFreeEnt > lMaxCode) - { - int nBytes = lNBits << 3; - lBitPos = (lBitPos - 1) + nBytes - (lBitPos - 1 + nBytes) % nBytes; - - lNBits++; - lMaxCode = (lNBits == maxBits) ? lMaxMaxCode : (1 << lNBits) - 1; - - lBitMask = (1 << lNBits) - 1; - lBitPos = ResetBuf(lBitPos); - goto MainLoop; - } - - #endregion A - - #region B - - // read next code - int pos = lBitPos >> 3; - int code = - ( - ( - (lData[pos] & 0xFF) - | ((lData[pos + 1] & 0xFF) << 8) - | ((lData[pos + 2] & 0xFF) << 16) - ) >> (lBitPos & 0x7) - ) & lBitMask; - - lBitPos += lNBits; - - // handle first iteration - if (lOldCode == -1) - { - if (code >= 256) - throw new IncompleteArchiveException( - "corrupt input: " + code + " > 255" - ); - - lFinChar = (byte)(lOldCode = code); - buffer[offset++] = lFinChar; - count--; - continue; - } - - // handle CLEAR code - if (code == TBL_CLEAR && blockMode) - { - Array.Copy(zeros, 0, lTabPrefix, 0, zeros.Length); - lFreeEnt = TBL_FIRST - 1; - - int nBytes = lNBits << 3; - lBitPos = (lBitPos - 1) + nBytes - (lBitPos - 1 + nBytes) % nBytes; - lNBits = LzwConstants.INIT_BITS; - lMaxCode = (1 << lNBits) - 1; - lBitMask = lMaxCode; - - // Code tables reset - - lBitPos = ResetBuf(lBitPos); - goto MainLoop; - } - - #endregion B - - #region C - - // setup - int inCode = code; - lStackP = lStack.Length; - - // Handle KwK case - if (code >= lFreeEnt) - { - if (code > lFreeEnt) - { - throw new IncompleteArchiveException( - "corrupt input: code=" + code + ", freeEnt=" + lFreeEnt - ); - } - - lStack[--lStackP] = lFinChar; - code = lOldCode; - } - - // Generate output characters in reverse order - while (code >= 256) - { - lStack[--lStackP] = lTabSuffix[code]; - code = lTabPrefix[code]; - } - - lFinChar = lTabSuffix[code]; - buffer[offset++] = lFinChar; - count--; - - // And put them out in forward order - sSize = lStack.Length - lStackP; - int num = (sSize >= count) ? count : sSize; - Array.Copy(lStack, lStackP, buffer, offset, num); - offset += num; - count -= num; - lStackP += num; - - #endregion C - - #region D - - // generate new entry in table - if (lFreeEnt < lMaxMaxCode) - { - lTabPrefix[lFreeEnt] = lOldCode; - lTabSuffix[lFreeEnt] = lFinChar; - lFreeEnt++; - } - - // Remember previous code - lOldCode = inCode; - - // if output buffer full, then return - if (count == 0) - { - nBits = lNBits; - maxCode = lMaxCode; - bitMask = lBitMask; - oldCode = lOldCode; - finChar = lFinChar; - stackP = lStackP; - freeEnt = lFreeEnt; - bitPos = lBitPos; - - return offset - start; - } - - #endregion D - } // while - - lBitPos = ResetBuf(lBitPos); - } while (got > 0); // do..while - - nBits = lNBits; - maxCode = lMaxCode; - bitMask = lBitMask; - oldCode = lOldCode; - finChar = lFinChar; - stackP = lStackP; - freeEnt = lFreeEnt; - bitPos = lBitPos; - - eof = true; - return offset - start; - } - - /// - /// Moves the unread data in the buffer to the beginning and resets - /// the pointers. - /// - /// - /// - private int ResetBuf(int bitPosition) - { - int pos = bitPosition >> 3; - Array.Copy(data, pos, data, 0, end - pos); - end -= pos; - return 0; - } - - private void Fill() - { - got = baseInputStream.Read(data, end, data.Length - 1 - end); - if (got > 0) - { - end += got; - } - } - - private void ParseHeader() - { - headerParsed = true; - byte[] hdr = new byte[LzwConstants.HDR_SIZE]; - int result = baseInputStream.Read(hdr, 0, hdr.Length); + int result = stream.Read(hdr, 0, hdr.Length); // Check the magic marker if (result < 0) @@ -414,211 +87,537 @@ namespace SharpCompress.Compressors.Lzw ) ); } + } + catch (Exception) + { + return false; + } + return true; + } - // Check the 3rd header byte - blockMode = (hdr[2] & LzwConstants.BLOCK_MODE_MASK) > 0; - maxBits = hdr[2] & LzwConstants.BIT_MASK; + /// + /// Gets or sets a flag indicating ownership of underlying stream. + /// When the flag is true will close the underlying stream also. + /// + /// The default value is true. + public bool IsStreamOwner { get; set; } = false; - if (maxBits > LzwConstants.MAX_BITS) + /// + /// Creates a LzwInputStream + /// + /// + /// The stream to read compressed data from (baseInputStream LZW format) + /// + public LzwStream(Stream baseInputStream) + { + this.baseInputStream = baseInputStream; +#if DEBUG_STREAMS + this.DebugConstruct(typeof(LzwStream)); +#endif + } + + /// + /// See + /// + /// + public override int ReadByte() + { + int b = Read(one, 0, 1); + if (b == 1) + return (one[0] & 0xff); + return -1; + } + + /// + /// Reads decompressed data into the provided buffer byte array + /// + /// + /// The array to read and decompress data into + /// + /// + /// The offset indicating where the data should be placed + /// + /// + /// The number of bytes to decompress + /// + /// The number of bytes read. Zero signals the end of stream + public override int Read(byte[] buffer, int offset, int count) + { + if (!headerParsed) + ParseHeader(); + + if (eof) + return 0; + + int start = offset; + + /* Using local copies of various variables speeds things up by as + * much as 30% in Java! Performance not tested in C#. + */ + int[] lTabPrefix = tabPrefix; + byte[] lTabSuffix = tabSuffix; + byte[] lStack = stack; + int lNBits = nBits; + int lMaxCode = maxCode; + int lMaxMaxCode = maxMaxCode; + int lBitMask = bitMask; + int lOldCode = oldCode; + byte lFinChar = finChar; + int lStackP = stackP; + int lFreeEnt = freeEnt; + byte[] lData = data; + int lBitPos = bitPos; + + // empty stack if stuff still left + int sSize = lStack.Length - lStackP; + if (sSize > 0) + { + int num = (sSize >= count) ? count : sSize; + Array.Copy(lStack, lStackP, buffer, offset, num); + offset += num; + count -= num; + lStackP += num; + } + + if (count == 0) + { + stackP = lStackP; + return offset - start; + } + + // loop, filling local buffer until enough data has been decompressed + MainLoop: + do + { + if (end < EXTRA) { - throw new ArchiveException( - "Stream compressed with " - + maxBits - + " bits, but decompression can only handle " - + LzwConstants.MAX_BITS - + " bits." - ); + Fill(); } - if ((hdr[2] & LzwConstants.RESERVED_MASK) > 0) + int bitIn = (got > 0) ? (end - end % lNBits) << 3 : (end << 3) - (lNBits - 1); + + while (lBitPos < bitIn) { - throw new ArchiveException("Unsupported bits set in the header."); - } + #region A - // Initialize variables - maxMaxCode = 1 << maxBits; - nBits = LzwConstants.INIT_BITS; - maxCode = (1 << nBits) - 1; - bitMask = maxCode; - oldCode = -1; - finChar = 0; - freeEnt = blockMode ? TBL_FIRST : 256; + // handle 1-byte reads correctly + if (count == 0) + { + nBits = lNBits; + maxCode = lMaxCode; + maxMaxCode = lMaxMaxCode; + bitMask = lBitMask; + oldCode = lOldCode; + finChar = lFinChar; + stackP = lStackP; + freeEnt = lFreeEnt; + bitPos = lBitPos; - tabPrefix = new int[1 << maxBits]; - tabSuffix = new byte[1 << maxBits]; - stack = new byte[1 << maxBits]; - stackP = stack.Length; + return offset - start; + } - for (int idx = 255; idx >= 0; idx--) - tabSuffix[idx] = (byte)idx; + // check for code-width expansion + if (lFreeEnt > lMaxCode) + { + int nBytes = lNBits << 3; + lBitPos = (lBitPos - 1) + nBytes - (lBitPos - 1 + nBytes) % nBytes; + + lNBits++; + lMaxCode = (lNBits == maxBits) ? lMaxMaxCode : (1 << lNBits) - 1; + + lBitMask = (1 << lNBits) - 1; + lBitPos = ResetBuf(lBitPos); + goto MainLoop; + } + + #endregion A + + #region B + + // read next code + int pos = lBitPos >> 3; + int code = + ( + ( + (lData[pos] & 0xFF) + | ((lData[pos + 1] & 0xFF) << 8) + | ((lData[pos + 2] & 0xFF) << 16) + ) >> (lBitPos & 0x7) + ) & lBitMask; + + lBitPos += lNBits; + + // handle first iteration + if (lOldCode == -1) + { + if (code >= 256) + throw new IncompleteArchiveException( + "corrupt input: " + code + " > 255" + ); + + lFinChar = (byte)(lOldCode = code); + buffer[offset++] = lFinChar; + count--; + continue; + } + + // handle CLEAR code + if (code == TBL_CLEAR && blockMode) + { + Array.Copy(zeros, 0, lTabPrefix, 0, zeros.Length); + lFreeEnt = TBL_FIRST - 1; + + int nBytes = lNBits << 3; + lBitPos = (lBitPos - 1) + nBytes - (lBitPos - 1 + nBytes) % nBytes; + lNBits = LzwConstants.INIT_BITS; + lMaxCode = (1 << lNBits) - 1; + lBitMask = lMaxCode; + + // Code tables reset + + lBitPos = ResetBuf(lBitPos); + goto MainLoop; + } + + #endregion B + + #region C + + // setup + int inCode = code; + lStackP = lStack.Length; + + // Handle KwK case + if (code >= lFreeEnt) + { + if (code > lFreeEnt) + { + throw new IncompleteArchiveException( + "corrupt input: code=" + code + ", freeEnt=" + lFreeEnt + ); + } + + lStack[--lStackP] = lFinChar; + code = lOldCode; + } + + // Generate output characters in reverse order + while (code >= 256) + { + lStack[--lStackP] = lTabSuffix[code]; + code = lTabPrefix[code]; + } + + lFinChar = lTabSuffix[code]; + buffer[offset++] = lFinChar; + count--; + + // And put them out in forward order + sSize = lStack.Length - lStackP; + int num = (sSize >= count) ? count : sSize; + Array.Copy(lStack, lStackP, buffer, offset, num); + offset += num; + count -= num; + lStackP += num; + + #endregion C + + #region D + + // generate new entry in table + if (lFreeEnt < lMaxMaxCode) + { + lTabPrefix[lFreeEnt] = lOldCode; + lTabSuffix[lFreeEnt] = lFinChar; + lFreeEnt++; + } + + // Remember previous code + lOldCode = inCode; + + // if output buffer full, then return + if (count == 0) + { + nBits = lNBits; + maxCode = lMaxCode; + bitMask = lBitMask; + oldCode = lOldCode; + finChar = lFinChar; + stackP = lStackP; + freeEnt = lFreeEnt; + bitPos = lBitPos; + + return offset - start; + } + + #endregion D + } // while + + lBitPos = ResetBuf(lBitPos); + } while (got > 0); // do..while + + nBits = lNBits; + maxCode = lMaxCode; + bitMask = lBitMask; + oldCode = lOldCode; + finChar = lFinChar; + stackP = lStackP; + freeEnt = lFreeEnt; + bitPos = lBitPos; + + eof = true; + return offset - start; + } + + /// + /// Moves the unread data in the buffer to the beginning and resets + /// the pointers. + /// + /// + /// + private int ResetBuf(int bitPosition) + { + int pos = bitPosition >> 3; + Array.Copy(data, pos, data, 0, end - pos); + end -= pos; + return 0; + } + + private void Fill() + { + got = baseInputStream.Read(data, end, data.Length - 1 - end); + if (got > 0) + { + end += got; + } + } + + private void ParseHeader() + { + headerParsed = true; + + byte[] hdr = new byte[LzwConstants.HDR_SIZE]; + + int result = baseInputStream.Read(hdr, 0, hdr.Length); + + // Check the magic marker + if (result < 0) + throw new IncompleteArchiveException("Failed to read LZW header"); + + if (hdr[0] != (LzwConstants.MAGIC >> 8) || hdr[1] != (LzwConstants.MAGIC & 0xff)) + { + throw new IncompleteArchiveException( + String.Format( + "Wrong LZW header. Magic bytes don't match. 0x{0:x2} 0x{1:x2}", + hdr[0], + hdr[1] + ) + ); } - #region Stream Overrides + // Check the 3rd header byte + blockMode = (hdr[2] & LzwConstants.BLOCK_MODE_MASK) > 0; + maxBits = hdr[2] & LzwConstants.BIT_MASK; - /// - /// Gets a value indicating whether the current stream supports reading - /// - public override bool CanRead + if (maxBits > LzwConstants.MAX_BITS) { - get { return baseInputStream.CanRead; } + throw new ArchiveException( + "Stream compressed with " + + maxBits + + " bits, but decompression can only handle " + + LzwConstants.MAX_BITS + + " bits." + ); } - /// - /// Gets a value of false indicating seeking is not supported for this stream. - /// - public override bool CanSeek + if ((hdr[2] & LzwConstants.RESERVED_MASK) > 0) { - get { return false; } + throw new ArchiveException("Unsupported bits set in the header."); } - /// - /// Gets a value of false indicating that this stream is not writeable. - /// - public override bool CanWrite - { - get { return false; } - } + // Initialize variables + maxMaxCode = 1 << maxBits; + nBits = LzwConstants.INIT_BITS; + maxCode = (1 << nBits) - 1; + bitMask = maxCode; + oldCode = -1; + finChar = 0; + freeEnt = blockMode ? TBL_FIRST : 256; - /// - /// A value representing the length of the stream in bytes. - /// - public override long Length - { - get { return got; } - } + tabPrefix = new int[1 << maxBits]; + tabSuffix = new byte[1 << maxBits]; + stack = new byte[1 << maxBits]; + stackP = stack.Length; - /// - /// The current position within the stream. - /// Throws a NotSupportedException when attempting to set the position - /// - /// Attempting to set the position - public override long Position - { - get { return baseInputStream.Position; } - set { throw new NotSupportedException("InflaterInputStream Position not supported"); } - } + for (int idx = 255; idx >= 0; idx--) + tabSuffix[idx] = (byte)idx; + } - /// - /// Flushes the baseInputStream - /// - public override void Flush() - { - baseInputStream.Flush(); - } + #region Stream Overrides - /// - /// Sets the position within the current stream - /// Always throws a NotSupportedException - /// - /// The relative offset to seek to. - /// The defining where to seek from. - /// The new position in the stream. - /// Any access - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException("Seek not supported"); - } + /// + /// Gets a value indicating whether the current stream supports reading + /// + public override bool CanRead + { + get { return baseInputStream.CanRead; } + } - /// - /// Set the length of the current stream - /// Always throws a NotSupportedException - /// - /// The new length value for the stream. - /// Any access - public override void SetLength(long value) - { - throw new NotSupportedException("InflaterInputStream SetLength not supported"); - } + /// + /// Gets a value of false indicating seeking is not supported for this stream. + /// + public override bool CanSeek + { + get { return false; } + } - /// - /// Writes a sequence of bytes to stream and advances the current position - /// This method always throws a NotSupportedException - /// - /// The buffer containing data to write. - /// The offset of the first byte to write. - /// The number of bytes to write. - /// Any access - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException("InflaterInputStream Write not supported"); - } + /// + /// Gets a value of false indicating that this stream is not writeable. + /// + public override bool CanWrite + { + get { return false; } + } - /// - /// Writes one byte to the current stream and advances the current position - /// Always throws a NotSupportedException - /// - /// The byte to write. - /// Any access - public override void WriteByte(byte value) - { - throw new NotSupportedException("InflaterInputStream WriteByte not supported"); - } + /// + /// A value representing the length of the stream in bytes. + /// + public override long Length + { + get { return got; } + } - /// - /// Closes the input stream. When - /// is true the underlying stream is also closed. - /// - protected override void Dispose(bool disposing) + /// + /// The current position within the stream. + /// Throws a NotSupportedException when attempting to set the position + /// + /// Attempting to set the position + public override long Position + { + get { return baseInputStream.Position; } + set { throw new NotSupportedException("InflaterInputStream Position not supported"); } + } + + /// + /// Flushes the baseInputStream + /// + public override void Flush() + { + baseInputStream.Flush(); + } + + /// + /// Sets the position within the current stream + /// Always throws a NotSupportedException + /// + /// The relative offset to seek to. + /// The defining where to seek from. + /// The new position in the stream. + /// Any access + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException("Seek not supported"); + } + + /// + /// Set the length of the current stream + /// Always throws a NotSupportedException + /// + /// The new length value for the stream. + /// Any access + public override void SetLength(long value) + { + throw new NotSupportedException("InflaterInputStream SetLength not supported"); + } + + /// + /// Writes a sequence of bytes to stream and advances the current position + /// This method always throws a NotSupportedException + /// + /// The buffer containing data to write. + /// The offset of the first byte to write. + /// The number of bytes to write. + /// Any access + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("InflaterInputStream Write not supported"); + } + + /// + /// Writes one byte to the current stream and advances the current position + /// Always throws a NotSupportedException + /// + /// The byte to write. + /// Any access + public override void WriteByte(byte value) + { + throw new NotSupportedException("InflaterInputStream WriteByte not supported"); + } + + /// + /// Closes the input stream. When + /// is true the underlying stream is also closed. + /// + protected override void Dispose(bool disposing) + { + if (!isClosed) { - if (!isClosed) - { - isClosed = true; + isClosed = true; #if DEBUG_STREAMS this.DebugDispose(typeof(LzwStream)); #endif - if (IsStreamOwner) - { - baseInputStream.Dispose(); - } + if (IsStreamOwner) + { + baseInputStream.Dispose(); } } - - #endregion Stream Overrides - - #region Instance Fields - - private Stream baseInputStream; - - /// - /// Flag indicating wether this instance has been closed or not. - /// - private bool isClosed; - - private readonly byte[] one = new byte[1]; - private bool headerParsed; - - // string table stuff - private const int TBL_CLEAR = 0x100; - - private const int TBL_FIRST = TBL_CLEAR + 1; - - private int[] tabPrefix = new int[0]; // - private byte[] tabSuffix = new byte[0]; // - private readonly int[] zeros = new int[256]; - private byte[] stack = new byte[0]; // - - // various state - private bool blockMode; - - private int nBits; - private int maxBits; - private int maxMaxCode; - private int maxCode; - private int bitMask; - private int oldCode; - private byte finChar; - private int stackP; - private int freeEnt; - - // input buffer - private readonly byte[] data = new byte[1024 * 8]; - - private int bitPos; - private int end; - private int got; - private bool eof; - private const int EXTRA = 64; - - #endregion Instance Fields } -} + + #endregion Stream Overrides + + #region Instance Fields + + private Stream baseInputStream; + + /// + /// Flag indicating wether this instance has been closed or not. + /// + private bool isClosed; + + private readonly byte[] one = new byte[1]; + private bool headerParsed; + + // string table stuff + private const int TBL_CLEAR = 0x100; + + private const int TBL_FIRST = TBL_CLEAR + 1; + + private int[] tabPrefix = new int[0]; // + private byte[] tabSuffix = new byte[0]; // + private readonly int[] zeros = new int[256]; + private byte[] stack = new byte[0]; // + + // various state + private bool blockMode; + + private int nBits; + private int maxBits; + private int maxMaxCode; + private int maxCode; + private int bitMask; + private int oldCode; + private byte finChar; + private int stackP; + private int freeEnt; + + // input buffer + private readonly byte[] data = new byte[1024 * 8]; + + private int bitPos; + private int end; + private int got; + private bool eof; + private const int EXTRA = 64; + + #endregion Instance Fields +} \ No newline at end of file diff --git a/src/SharpCompress/Compressors/RLE90/RLE.cs b/src/SharpCompress/Compressors/RLE90/RLE.cs index 8bc8ee1b..5f51d5ca 100644 --- a/src/SharpCompress/Compressors/RLE90/RLE.cs +++ b/src/SharpCompress/Compressors/RLE90/RLE.cs @@ -1,52 +1,51 @@ using System.Collections.Generic; using System.Linq; -namespace SharpCompress.Compressors.RLE90 +namespace SharpCompress.Compressors.RLE90; + +public static class RLE { - public static class RLE + private const byte DLE = 0x90; + + /// + /// Unpacks an RLE compressed buffer. + /// Format: DLE , where count == 0 -> DLE + /// + /// The compressed buffer to unpack. + /// A list of unpacked bytes. + public static List UnpackRLE(byte[] compressedBuffer) { - private const byte DLE = 0x90; + var result = new List(compressedBuffer.Length * 2); // Optimized initial capacity + var countMode = false; + byte last = 0; - /// - /// Unpacks an RLE compressed buffer. - /// Format: DLE , where count == 0 -> DLE - /// - /// The compressed buffer to unpack. - /// A list of unpacked bytes. - public static List UnpackRLE(byte[] compressedBuffer) + foreach (var c in compressedBuffer) { - var result = new List(compressedBuffer.Length * 2); // Optimized initial capacity - var countMode = false; - byte last = 0; - - foreach (var c in compressedBuffer) + if (!countMode) { - if (!countMode) + if (c == DLE) { - if (c == DLE) - { - countMode = true; - } - else - { - result.Add(c); - last = c; - } + countMode = true; } else { - countMode = false; - if (c == 0) - { - result.Add(DLE); - } - else - { - result.AddRange(Enumerable.Repeat(last, c - 1)); - } + result.Add(c); + last = c; + } + } + else + { + countMode = false; + if (c == 0) + { + result.Add(DLE); + } + else + { + result.AddRange(Enumerable.Repeat(last, c - 1)); } } - return result; } + return result; } -} +} \ No newline at end of file diff --git a/src/SharpCompress/Compressors/RLE90/RunLength90Stream.cs b/src/SharpCompress/Compressors/RLE90/RunLength90Stream.cs index 09034040..a04f4984 100644 --- a/src/SharpCompress/Compressors/RLE90/RunLength90Stream.cs +++ b/src/SharpCompress/Compressors/RLE90/RunLength90Stream.cs @@ -6,91 +6,90 @@ using System.Text; using System.Threading.Tasks; using SharpCompress.IO; -namespace SharpCompress.Compressors.RLE90 +namespace SharpCompress.Compressors.RLE90; + +public class RunLength90Stream : Stream, IStreamStack { - public class RunLength90Stream : Stream, IStreamStack +#if DEBUG_STREAMS + long IStreamStack.InstanceId { get; set; } +#endif + int IStreamStack.DefaultBufferSize { get; set; } + + Stream IStreamStack.BaseStream() => _stream; + + int IStreamStack.BufferSize + { + get => 0; + set { } + } + int IStreamStack.BufferPosition + { + get => 0; + set { } + } + + void IStreamStack.SetPosition(long position) { } + + private readonly Stream _stream; + private const byte DLE = 0x90; + private int _compressedSize; + private bool _processed = false; + + public RunLength90Stream(Stream stream, int compressedSize) + { + _stream = stream; + _compressedSize = compressedSize; +#if DEBUG_STREAMS + this.DebugConstruct(typeof(RunLength90Stream)); +#endif + } + + protected override void Dispose(bool disposing) { #if DEBUG_STREAMS - long IStreamStack.InstanceId { get; set; } + this.DebugDispose(typeof(RunLength90Stream)); #endif - int IStreamStack.DefaultBufferSize { get; set; } - - Stream IStreamStack.BaseStream() => _stream; - - int IStreamStack.BufferSize - { - get => 0; - set { } - } - int IStreamStack.BufferPosition - { - get => 0; - set { } - } - - void IStreamStack.SetPosition(long position) { } - - private readonly Stream _stream; - private const byte DLE = 0x90; - private int _compressedSize; - private bool _processed = false; - - public RunLength90Stream(Stream stream, int compressedSize) - { - _stream = stream; - _compressedSize = compressedSize; -#if DEBUG_STREAMS - this.DebugConstruct(typeof(RunLength90Stream)); -#endif - } - - protected override void Dispose(bool disposing) - { -#if DEBUG_STREAMS - this.DebugDispose(typeof(RunLength90Stream)); -#endif - base.Dispose(disposing); - } - - public override bool CanRead => true; - - public override bool CanSeek => false; - - public override bool CanWrite => false; - - public override long Length => throw new NotImplementedException(); - - public override long Position - { - get => _stream.Position; - set => throw new NotImplementedException(); - } - - public override void Flush() => throw new NotImplementedException(); - - public override int Read(byte[] buffer, int offset, int count) - { - if (_processed) - { - return 0; - } - _processed = true; - - using var binaryReader = new BinaryReader(_stream); - byte[] compressedBuffer = binaryReader.ReadBytes(_compressedSize); - - var unpacked = RLE.UnpackRLE(compressedBuffer); - unpacked.CopyTo(buffer); - - return unpacked.Count; - } - - public override long Seek(long offset, SeekOrigin origin) => - throw new NotImplementedException(); - - public override void SetLength(long value) => throw new NotImplementedException(); - - public override void Write(byte[] buffer, int offset, int count) => - throw new NotImplementedException(); + base.Dispose(disposing); } -} + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => throw new NotImplementedException(); + + public override long Position + { + get => _stream.Position; + set => throw new NotImplementedException(); + } + + public override void Flush() => throw new NotImplementedException(); + + public override int Read(byte[] buffer, int offset, int count) + { + if (_processed) + { + return 0; + } + _processed = true; + + using var binaryReader = new BinaryReader(_stream); + byte[] compressedBuffer = binaryReader.ReadBytes(_compressedSize); + + var unpacked = RLE.UnpackRLE(compressedBuffer); + unpacked.CopyTo(buffer); + + return unpacked.Count; + } + + public override long Seek(long offset, SeekOrigin origin) => + throw new NotImplementedException(); + + public override void SetLength(long value) => throw new NotImplementedException(); + + public override void Write(byte[] buffer, int offset, int count) => + throw new NotImplementedException(); +} \ No newline at end of file diff --git a/src/SharpCompress/Compressors/Shrink/BitStream.cs b/src/SharpCompress/Compressors/Shrink/BitStream.cs index 8bb69ead..d5deba7e 100644 --- a/src/SharpCompress/Compressors/Shrink/BitStream.cs +++ b/src/SharpCompress/Compressors/Shrink/BitStream.cs @@ -1,79 +1,78 @@ -namespace SharpCompress.Compressors.Shrink +namespace SharpCompress.Compressors.Shrink; + +internal class BitStream { - internal class BitStream + private byte[] _src; + private int _srcLen; + private int _byteIdx; + private int _bitIdx; + private int _bitsLeft; + private ulong _bitBuffer; + private static uint[] _maskBits = new uint[17] { - private byte[] _src; - private int _srcLen; - private int _byteIdx; - private int _bitIdx; - private int _bitsLeft; - private ulong _bitBuffer; - private static uint[] _maskBits = new uint[17] - { - 0U, - 1U, - 3U, - 7U, - 15U, - 31U, - 63U, - (uint)sbyte.MaxValue, - (uint)byte.MaxValue, - 511U, - 1023U, - 2047U, - 4095U, - 8191U, - 16383U, - (uint)short.MaxValue, - (uint)ushort.MaxValue, - }; + 0U, + 1U, + 3U, + 7U, + 15U, + 31U, + 63U, + (uint)sbyte.MaxValue, + (uint)byte.MaxValue, + 511U, + 1023U, + 2047U, + 4095U, + 8191U, + 16383U, + (uint)short.MaxValue, + (uint)ushort.MaxValue, + }; - public BitStream(byte[] src, int srcLen) - { - _src = src; - _srcLen = srcLen; - _byteIdx = 0; - _bitIdx = 0; - } - - public int BytesRead => (_byteIdx << 3) + _bitIdx; - - private int NextByte() - { - if (_byteIdx >= _srcLen) - { - return 0; - } - - return _src[_byteIdx++]; - } - - public int NextBits(int nbits) - { - var result = 0; - if (nbits > _bitsLeft) - { - int num; - while (_bitsLeft <= 24 && (num = NextByte()) != 1234) - { - _bitBuffer |= (ulong)num << _bitsLeft; - _bitsLeft += 8; - } - } - result = (int)((long)_bitBuffer & (long)_maskBits[nbits]); - _bitBuffer >>= nbits; - _bitsLeft -= nbits; - return result; - } - - public bool Advance(int count) - { - if (_byteIdx > _srcLen) - { - return false; - } - return true; - } + public BitStream(byte[] src, int srcLen) + { + _src = src; + _srcLen = srcLen; + _byteIdx = 0; + _bitIdx = 0; } -} + + public int BytesRead => (_byteIdx << 3) + _bitIdx; + + private int NextByte() + { + if (_byteIdx >= _srcLen) + { + return 0; + } + + return _src[_byteIdx++]; + } + + public int NextBits(int nbits) + { + var result = 0; + if (nbits > _bitsLeft) + { + int num; + while (_bitsLeft <= 24 && (num = NextByte()) != 1234) + { + _bitBuffer |= (ulong)num << _bitsLeft; + _bitsLeft += 8; + } + } + result = (int)((long)_bitBuffer & (long)_maskBits[nbits]); + _bitBuffer >>= nbits; + _bitsLeft -= nbits; + return result; + } + + public bool Advance(int count) + { + if (_byteIdx > _srcLen) + { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/SharpCompress/Compressors/Shrink/HwUnshrink.cs b/src/SharpCompress/Compressors/Shrink/HwUnshrink.cs index 4506ccb1..7800c111 100644 --- a/src/SharpCompress/Compressors/Shrink/HwUnshrink.cs +++ b/src/SharpCompress/Compressors/Shrink/HwUnshrink.cs @@ -1,275 +1,297 @@ using System; -namespace SharpCompress.Compressors.Shrink +namespace SharpCompress.Compressors.Shrink; + +public class HwUnshrink { - public class HwUnshrink + private const int MIN_CODE_SIZE = 9; + private const int MAX_CODE_SIZE = 13; + + private const ushort MAX_CODE = (ushort)((1U << MAX_CODE_SIZE) - 1); + private const ushort INVALID_CODE = ushort.MaxValue; + private const ushort CONTROL_CODE = 256; + private const ushort INC_CODE_SIZE = 1; + private const ushort PARTIAL_CLEAR = 2; + + private const int HASH_BITS = MAX_CODE_SIZE + 1; // For a load factor of 0.5. + private const int HASHTAB_SIZE = 1 << HASH_BITS; + private const ushort UNKNOWN_LEN = ushort.MaxValue; + + private struct CodeTabEntry { - private const int MIN_CODE_SIZE = 9; - private const int MAX_CODE_SIZE = 13; + public int prefixCode; // INVALID_CODE means the entry is invalid. + public byte extByte; + public ushort len; + public int lastDstPos; + } - private const ushort MAX_CODE = (ushort)((1U << MAX_CODE_SIZE) - 1); - private const ushort INVALID_CODE = ushort.MaxValue; - private const ushort CONTROL_CODE = 256; - private const ushort INC_CODE_SIZE = 1; - private const ushort PARTIAL_CLEAR = 2; - - private const int HASH_BITS = MAX_CODE_SIZE + 1; // For a load factor of 0.5. - private const int HASHTAB_SIZE = 1 << HASH_BITS; - private const ushort UNKNOWN_LEN = ushort.MaxValue; - - private struct CodeTabEntry + private static void CodeTabInit(CodeTabEntry[] codeTab) + { + for (var i = 0; i <= byte.MaxValue; i++) { - public int prefixCode; // INVALID_CODE means the entry is invalid. - public byte extByte; - public ushort len; - public int lastDstPos; + codeTab[i].prefixCode = (ushort)i; + codeTab[i].extByte = (byte)i; + codeTab[i].len = 1; } - private static void CodeTabInit(CodeTabEntry[] codeTab) + for (var i = byte.MaxValue + 1; i <= MAX_CODE; i++) { - for (var i = 0; i <= byte.MaxValue; i++) - { - codeTab[i].prefixCode = (ushort)i; - codeTab[i].extByte = (byte)i; - codeTab[i].len = 1; - } + codeTab[i].prefixCode = INVALID_CODE; + } + } - for (var i = byte.MaxValue + 1; i <= MAX_CODE; i++) + private static void UnshrinkPartialClear(CodeTabEntry[] codeTab, ref CodeQueue queue) + { + var isPrefix = new bool[MAX_CODE + 1]; + int codeQueueSize; + + // Scan for codes that have been used as a prefix. + for (var i = CONTROL_CODE + 1; i <= MAX_CODE; i++) + { + if (codeTab[i].prefixCode != INVALID_CODE) + { + isPrefix[codeTab[i].prefixCode] = true; + } + } + + // Clear "non-prefix" codes in the table; populate the code queue. + codeQueueSize = 0; + for (var i = CONTROL_CODE + 1; i <= MAX_CODE; i++) + { + if (!isPrefix[i]) { codeTab[i].prefixCode = INVALID_CODE; + queue.codes[codeQueueSize++] = (ushort)i; } } - private static void UnshrinkPartialClear(CodeTabEntry[] codeTab, ref CodeQueue queue) + queue.codes[codeQueueSize] = INVALID_CODE; // End-of-queue marker. + queue.nextIdx = 0; + } + + private static bool ReadCode( + BitStream stream, + ref int codeSize, + CodeTabEntry[] codeTab, + ref CodeQueue queue, + out int nextCode + ) + { + int code, + controlCode; + + code = (int)stream.NextBits(codeSize); + if (!stream.Advance(codeSize)) { - var isPrefix = new bool[MAX_CODE + 1]; - int codeQueueSize; - - // Scan for codes that have been used as a prefix. - for (var i = CONTROL_CODE + 1; i <= MAX_CODE; i++) - { - if (codeTab[i].prefixCode != INVALID_CODE) - { - isPrefix[codeTab[i].prefixCode] = true; - } - } - - // Clear "non-prefix" codes in the table; populate the code queue. - codeQueueSize = 0; - for (var i = CONTROL_CODE + 1; i <= MAX_CODE; i++) - { - if (!isPrefix[i]) - { - codeTab[i].prefixCode = INVALID_CODE; - queue.codes[codeQueueSize++] = (ushort)i; - } - } - - queue.codes[codeQueueSize] = INVALID_CODE; // End-of-queue marker. - queue.nextIdx = 0; + nextCode = INVALID_CODE; + return false; } - private static bool ReadCode( - BitStream stream, - ref int codeSize, - CodeTabEntry[] codeTab, - ref CodeQueue queue, - out int nextCode - ) + // Handle regular codes (the common case). + if (code != CONTROL_CODE) { - int code, - controlCode; - - code = (int)stream.NextBits(codeSize); - if (!stream.Advance(codeSize)) - { - nextCode = INVALID_CODE; - return false; - } - - // Handle regular codes (the common case). - if (code != CONTROL_CODE) - { - nextCode = code; - return true; - } - - // Handle control codes. - controlCode = (ushort)stream.NextBits(codeSize); - if (!stream.Advance(codeSize)) - { - nextCode = INVALID_CODE; - return true; - } - - if (controlCode == INC_CODE_SIZE && codeSize < MAX_CODE_SIZE) - { - codeSize++; - return ReadCode(stream, ref codeSize, codeTab, ref queue, out nextCode); - } - - if (controlCode == PARTIAL_CLEAR) - { - UnshrinkPartialClear(codeTab, ref queue); - return ReadCode(stream, ref codeSize, codeTab, ref queue, out nextCode); - } + nextCode = code; + return true; + } + // Handle control codes. + controlCode = (ushort)stream.NextBits(codeSize); + if (!stream.Advance(codeSize)) + { nextCode = INVALID_CODE; return true; } - private static void CopyFromPrevPos(byte[] dst, int prevPos, int dstPos, int len) + if (controlCode == INC_CODE_SIZE && codeSize < MAX_CODE_SIZE) { - if (dstPos + len > dst.Length) - { - // Not enough room in dst for the sloppy copy below. - Array.Copy(dst, prevPos, dst, dstPos, len); - return; - } - - if (prevPos + len > dstPos) - { - // Benign one-byte overlap possible in the KwKwK case. - //assert(prevPos + len == dstPos + 1); - //assert(dst[prevPos] == dst[prevPos + len - 1]); - } - - Buffer.BlockCopy(dst, prevPos, dst, dstPos, len); + codeSize++; + return ReadCode(stream, ref codeSize, codeTab, ref queue, out nextCode); } - private static UnshrnkStatus OutputCode( - int code, - byte[] dst, - int dstPos, - int dstCap, - int prevCode, - CodeTabEntry[] codeTab, - ref CodeQueue queue, - out byte firstByte, - out int len - ) + if (controlCode == PARTIAL_CLEAR) { - int prefixCode; + UnshrinkPartialClear(codeTab, ref queue); + return ReadCode(stream, ref codeSize, codeTab, ref queue, out nextCode); + } - //assert(code <= MAX_CODE && code != CONTROL_CODE); - //assert(dstPos < dstCap); + nextCode = INVALID_CODE; + return true; + } + + private static void CopyFromPrevPos(byte[] dst, int prevPos, int dstPos, int len) + { + if (dstPos + len > dst.Length) + { + // Not enough room in dst for the sloppy copy below. + Array.Copy(dst, prevPos, dst, dstPos, len); + return; + } + + if (prevPos + len > dstPos) + { + // Benign one-byte overlap possible in the KwKwK case. + //assert(prevPos + len == dstPos + 1); + //assert(dst[prevPos] == dst[prevPos + len - 1]); + } + + Buffer.BlockCopy(dst, prevPos, dst, dstPos, len); + } + + private static UnshrnkStatus OutputCode( + int code, + byte[] dst, + int dstPos, + int dstCap, + int prevCode, + CodeTabEntry[] codeTab, + ref CodeQueue queue, + out byte firstByte, + out int len + ) + { + int prefixCode; + + //assert(code <= MAX_CODE && code != CONTROL_CODE); + //assert(dstPos < dstCap); + firstByte = 0; + if (code <= byte.MaxValue) + { + // Output literal byte. + firstByte = (byte)code; + len = 1; + dst[dstPos] = (byte)code; + return UnshrnkStatus.Ok; + } + + if (codeTab[code].prefixCode == INVALID_CODE || codeTab[code].prefixCode == code) + { + // Reject invalid codes. Self-referential codes may exist in the table but cannot be used. firstByte = 0; - if (code <= byte.MaxValue) - { - // Output literal byte. - firstByte = (byte)code; - len = 1; - dst[dstPos] = (byte)code; - return UnshrnkStatus.Ok; - } + len = 0; + return UnshrnkStatus.Error; + } - if (codeTab[code].prefixCode == INVALID_CODE || codeTab[code].prefixCode == code) - { - // Reject invalid codes. Self-referential codes may exist in the table but cannot be used. - firstByte = 0; - len = 0; - return UnshrnkStatus.Error; - } - - if (codeTab[code].len != UNKNOWN_LEN) - { - // Output string with known length (the common case). - if (dstCap - dstPos < codeTab[code].len) - { - firstByte = 0; - len = 0; - return UnshrnkStatus.Full; - } - - CopyFromPrevPos(dst, codeTab[code].lastDstPos, dstPos, codeTab[code].len); - firstByte = dst[dstPos]; - len = codeTab[code].len; - return UnshrnkStatus.Ok; - } - - // Output a string of unknown length. - //assert(codeTab[code].len == UNKNOWN_LEN); - prefixCode = codeTab[code].prefixCode; - // assert(prefixCode > CONTROL_CODE); - - if (prefixCode == queue.codes[queue.nextIdx]) - { - // The prefix code hasn't been added yet, but we were just about to: the KwKwK case. - //assert(codeTab[prevCode].prefixCode != INVALID_CODE); - codeTab[prefixCode].prefixCode = prevCode; - codeTab[prefixCode].extByte = firstByte; - codeTab[prefixCode].len = (ushort)(codeTab[prevCode].len + 1); - codeTab[prefixCode].lastDstPos = codeTab[prevCode].lastDstPos; - dst[dstPos] = firstByte; - } - else if (codeTab[prefixCode].prefixCode == INVALID_CODE) - { - // The prefix code is still invalid. - firstByte = 0; - len = 0; - return UnshrnkStatus.Error; - } - - // Output the prefix string, then the extension byte. - len = codeTab[prefixCode].len + 1; - if (dstCap - dstPos < len) + if (codeTab[code].len != UNKNOWN_LEN) + { + // Output string with known length (the common case). + if (dstCap - dstPos < codeTab[code].len) { firstByte = 0; len = 0; return UnshrnkStatus.Full; } - CopyFromPrevPos(dst, codeTab[prefixCode].lastDstPos, dstPos, codeTab[prefixCode].len); - dst[dstPos + len - 1] = codeTab[code].extByte; + CopyFromPrevPos(dst, codeTab[code].lastDstPos, dstPos, codeTab[code].len); firstByte = dst[dstPos]; - - // Update the code table now that the string has a length and pos. - //assert(prevCode != code); - codeTab[code].len = (ushort)len; - codeTab[code].lastDstPos = dstPos; - + len = codeTab[code].len; return UnshrnkStatus.Ok; } - public static UnshrnkStatus Unshrink( - byte[] src, - int srcLen, - out int srcUsed, - byte[] dst, - int dstCap, - out int dstUsed - ) + // Output a string of unknown length. + //assert(codeTab[code].len == UNKNOWN_LEN); + prefixCode = codeTab[code].prefixCode; + // assert(prefixCode > CONTROL_CODE); + + if (prefixCode == queue.codes[queue.nextIdx]) { - var codeTab = new CodeTabEntry[HASHTAB_SIZE]; - var queue = new CodeQueue(); - var stream = new BitStream(src, srcLen); - int codeSize, - dstPos, - len; - int currCode, - prevCode, - newCode; - byte firstByte; + // The prefix code hasn't been added yet, but we were just about to: the KwKwK case. + //assert(codeTab[prevCode].prefixCode != INVALID_CODE); + codeTab[prefixCode].prefixCode = prevCode; + codeTab[prefixCode].extByte = firstByte; + codeTab[prefixCode].len = (ushort)(codeTab[prevCode].len + 1); + codeTab[prefixCode].lastDstPos = codeTab[prevCode].lastDstPos; + dst[dstPos] = firstByte; + } + else if (codeTab[prefixCode].prefixCode == INVALID_CODE) + { + // The prefix code is still invalid. + firstByte = 0; + len = 0; + return UnshrnkStatus.Error; + } - CodeTabInit(codeTab); - CodeQueueInit(ref queue); - codeSize = MIN_CODE_SIZE; - dstPos = 0; + // Output the prefix string, then the extension byte. + len = codeTab[prefixCode].len + 1; + if (dstCap - dstPos < len) + { + firstByte = 0; + len = 0; + return UnshrnkStatus.Full; + } - // Handle the first code separately since there is no previous code. - if (!ReadCode(stream, ref codeSize, codeTab, ref queue, out currCode)) + CopyFromPrevPos(dst, codeTab[prefixCode].lastDstPos, dstPos, codeTab[prefixCode].len); + dst[dstPos + len - 1] = codeTab[code].extByte; + firstByte = dst[dstPos]; + + // Update the code table now that the string has a length and pos. + //assert(prevCode != code); + codeTab[code].len = (ushort)len; + codeTab[code].lastDstPos = dstPos; + + return UnshrnkStatus.Ok; + } + + public static UnshrnkStatus Unshrink( + byte[] src, + int srcLen, + out int srcUsed, + byte[] dst, + int dstCap, + out int dstUsed + ) + { + var codeTab = new CodeTabEntry[HASHTAB_SIZE]; + var queue = new CodeQueue(); + var stream = new BitStream(src, srcLen); + int codeSize, + dstPos, + len; + int currCode, + prevCode, + newCode; + byte firstByte; + + CodeTabInit(codeTab); + CodeQueueInit(ref queue); + codeSize = MIN_CODE_SIZE; + dstPos = 0; + + // Handle the first code separately since there is no previous code. + if (!ReadCode(stream, ref codeSize, codeTab, ref queue, out currCode)) + { + srcUsed = stream.BytesRead; + dstUsed = 0; + return UnshrnkStatus.Ok; + } + + //assert(currCode != CONTROL_CODE); + if (currCode > byte.MaxValue) + { + srcUsed = stream.BytesRead; + dstUsed = 0; + return UnshrnkStatus.Error; // The first code must be a literal. + } + + if (dstPos == dstCap) + { + srcUsed = stream.BytesRead; + dstUsed = 0; + return UnshrnkStatus.Full; + } + + firstByte = (byte)currCode; + dst[dstPos] = (byte)currCode; + codeTab[currCode].lastDstPos = dstPos; + dstPos++; + + prevCode = currCode; + while (ReadCode(stream, ref codeSize, codeTab, ref queue, out currCode)) + { + if (currCode == INVALID_CODE) { srcUsed = stream.BytesRead; dstUsed = 0; - return UnshrnkStatus.Ok; - } - - //assert(currCode != CONTROL_CODE); - if (currCode > byte.MaxValue) - { - srcUsed = stream.BytesRead; - dstUsed = 0; - return UnshrnkStatus.Error; // The first code must be a literal. + return UnshrnkStatus.Error; } if (dstPos == dstCap) @@ -279,153 +301,130 @@ namespace SharpCompress.Compressors.Shrink return UnshrnkStatus.Full; } - firstByte = (byte)currCode; - dst[dstPos] = (byte)currCode; - codeTab[currCode].lastDstPos = dstPos; - dstPos++; - - prevCode = currCode; - while (ReadCode(stream, ref codeSize, codeTab, ref queue, out currCode)) + // Handle KwKwK: next code used before being added. + if (currCode == queue.codes[queue.nextIdx]) { - if (currCode == INVALID_CODE) + if (codeTab[prevCode].prefixCode == INVALID_CODE) { + // The previous code is no longer valid. srcUsed = stream.BytesRead; dstUsed = 0; return UnshrnkStatus.Error; } - if (dstPos == dstCap) - { - srcUsed = stream.BytesRead; - dstUsed = 0; - return UnshrnkStatus.Full; - } - - // Handle KwKwK: next code used before being added. - if (currCode == queue.codes[queue.nextIdx]) - { - if (codeTab[prevCode].prefixCode == INVALID_CODE) - { - // The previous code is no longer valid. - srcUsed = stream.BytesRead; - dstUsed = 0; - return UnshrnkStatus.Error; - } - - // Extend the previous code with its first byte. - //assert(currCode != prevCode); - codeTab[currCode].prefixCode = prevCode; - codeTab[currCode].extByte = firstByte; - codeTab[currCode].len = (ushort)(codeTab[prevCode].len + 1); - codeTab[currCode].lastDstPos = codeTab[prevCode].lastDstPos; - //assert(dstPos < dstCap); - dst[dstPos] = firstByte; - } - - // Output the string represented by the current code. - var status = OutputCode( - currCode, - dst, - dstPos, - dstCap, - prevCode, - codeTab, - ref queue, - out firstByte, - out len - ); - if (status != UnshrnkStatus.Ok) - { - srcUsed = stream.BytesRead; - dstUsed = 0; - return status; - } - - // Verify that the output matches walking the prefixes. - var c = currCode; - for (var i = 0; i < len; i++) - { - // assert(codeTab[c].len == len - i); - //assert(codeTab[c].extByte == dst[dstPos + len - i - 1]); - c = codeTab[c].prefixCode; - } - - // Add a new code to the string table if there's room. - // The string is the previous code's string extended with the first byte of the current code's string. - newCode = CodeQueueRemoveNext(ref queue); - if (newCode != INVALID_CODE) - { - //assert(codeTab[prevCode].lastDstPos < dstPos); - codeTab[newCode].prefixCode = prevCode; - codeTab[newCode].extByte = firstByte; - codeTab[newCode].len = (ushort)(codeTab[prevCode].len + 1); - codeTab[newCode].lastDstPos = codeTab[prevCode].lastDstPos; - - if (codeTab[prevCode].prefixCode == INVALID_CODE) - { - // prevCode was invalidated in a partial clearing. Until that code is re-used, the - // string represented by newCode is indeterminate. - codeTab[newCode].len = UNKNOWN_LEN; - } - // If prevCode was invalidated in a partial clearing, it's possible that newCode == prevCode, - // in which case it will never be used or cleared. - } - - codeTab[currCode].lastDstPos = dstPos; - dstPos += len; - - prevCode = currCode; + // Extend the previous code with its first byte. + //assert(currCode != prevCode); + codeTab[currCode].prefixCode = prevCode; + codeTab[currCode].extByte = firstByte; + codeTab[currCode].len = (ushort)(codeTab[prevCode].len + 1); + codeTab[currCode].lastDstPos = codeTab[prevCode].lastDstPos; + //assert(dstPos < dstCap); + dst[dstPos] = firstByte; } - srcUsed = stream.BytesRead; - dstUsed = dstPos; - - return UnshrnkStatus.Ok; - } - - public enum UnshrnkStatus - { - Ok, - Full, - Error, - } - - private struct CodeQueue - { - public int nextIdx; - public ushort[] codes; - } - - private static void CodeQueueInit(ref CodeQueue q) - { - int codeQueueSize; - ushort code; - - codeQueueSize = 0; - q.codes = new ushort[MAX_CODE - CONTROL_CODE + 2]; - - for (code = CONTROL_CODE + 1; code <= MAX_CODE; code++) + // Output the string represented by the current code. + var status = OutputCode( + currCode, + dst, + dstPos, + dstCap, + prevCode, + codeTab, + ref queue, + out firstByte, + out len + ); + if (status != UnshrnkStatus.Ok) { - q.codes[codeQueueSize++] = code; + srcUsed = stream.BytesRead; + dstUsed = 0; + return status; } - //assert(codeQueueSize < q.codes.Length); - q.codes[codeQueueSize] = INVALID_CODE; // End-of-queue marker. - q.nextIdx = 0; - } - - private static ushort CodeQueueNext(ref CodeQueue q) => - //assert(q.nextIdx < q.codes.Length); - q.codes[q.nextIdx]; - - private static ushort CodeQueueRemoveNext(ref CodeQueue q) - { - var code = CodeQueueNext(ref q); - if (code != INVALID_CODE) + // Verify that the output matches walking the prefixes. + var c = currCode; + for (var i = 0; i < len; i++) { - q.nextIdx++; + // assert(codeTab[c].len == len - i); + //assert(codeTab[c].extByte == dst[dstPos + len - i - 1]); + c = codeTab[c].prefixCode; } - return code; + + // Add a new code to the string table if there's room. + // The string is the previous code's string extended with the first byte of the current code's string. + newCode = CodeQueueRemoveNext(ref queue); + if (newCode != INVALID_CODE) + { + //assert(codeTab[prevCode].lastDstPos < dstPos); + codeTab[newCode].prefixCode = prevCode; + codeTab[newCode].extByte = firstByte; + codeTab[newCode].len = (ushort)(codeTab[prevCode].len + 1); + codeTab[newCode].lastDstPos = codeTab[prevCode].lastDstPos; + + if (codeTab[prevCode].prefixCode == INVALID_CODE) + { + // prevCode was invalidated in a partial clearing. Until that code is re-used, the + // string represented by newCode is indeterminate. + codeTab[newCode].len = UNKNOWN_LEN; + } + // If prevCode was invalidated in a partial clearing, it's possible that newCode == prevCode, + // in which case it will never be used or cleared. + } + + codeTab[currCode].lastDstPos = dstPos; + dstPos += len; + + prevCode = currCode; } + + srcUsed = stream.BytesRead; + dstUsed = dstPos; + + return UnshrnkStatus.Ok; } -} + + public enum UnshrnkStatus + { + Ok, + Full, + Error, + } + + private struct CodeQueue + { + public int nextIdx; + public ushort[] codes; + } + + private static void CodeQueueInit(ref CodeQueue q) + { + int codeQueueSize; + ushort code; + + codeQueueSize = 0; + q.codes = new ushort[MAX_CODE - CONTROL_CODE + 2]; + + for (code = CONTROL_CODE + 1; code <= MAX_CODE; code++) + { + q.codes[codeQueueSize++] = code; + } + + //assert(codeQueueSize < q.codes.Length); + q.codes[codeQueueSize] = INVALID_CODE; // End-of-queue marker. + q.nextIdx = 0; + } + + private static ushort CodeQueueNext(ref CodeQueue q) => + //assert(q.nextIdx < q.codes.Length); + q.codes[q.nextIdx]; + + private static ushort CodeQueueRemoveNext(ref CodeQueue q) + { + var code = CodeQueueNext(ref q); + if (code != INVALID_CODE) + { + q.nextIdx++; + } + return code; + } +} \ No newline at end of file diff --git a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs index bd9760df..2a31cbdc 100644 --- a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs +++ b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs @@ -7,139 +7,138 @@ using System.Threading.Tasks; using SharpCompress.Compressors.RLE90; using SharpCompress.IO; -namespace SharpCompress.Compressors.Squeezed +namespace SharpCompress.Compressors.Squeezed; + +public class SqueezeStream : Stream, IStreamStack { - public class SqueezeStream : Stream, IStreamStack - { #if DEBUG_STREAMS long IStreamStack.InstanceId { get; set; } #endif - int IStreamStack.DefaultBufferSize { get; set; } + int IStreamStack.DefaultBufferSize { get; set; } - Stream IStreamStack.BaseStream() => _stream; + Stream IStreamStack.BaseStream() => _stream; - int IStreamStack.BufferSize - { - get => 0; - set { } - } - int IStreamStack.BufferPosition - { - get => 0; - set { } - } + int IStreamStack.BufferSize + { + get => 0; + set { } + } + int IStreamStack.BufferPosition + { + get => 0; + set { } + } - void IStreamStack.SetPosition(long position) { } + void IStreamStack.SetPosition(long position) { } - private readonly Stream _stream; - private readonly int _compressedSize; - private const int NUMVALS = 257; - private const int SPEOF = 256; - private bool _processed = false; + private readonly Stream _stream; + private readonly int _compressedSize; + private const int NUMVALS = 257; + private const int SPEOF = 256; + private bool _processed = false; - public SqueezeStream(Stream stream, int compressedSize) - { - _stream = stream; - _compressedSize = compressedSize; + public SqueezeStream(Stream stream, int compressedSize) + { + _stream = stream; + _compressedSize = compressedSize; #if DEBUG_STREAMS this.DebugConstruct(typeof(SqueezeStream)); #endif - } + } - protected override void Dispose(bool disposing) - { + protected override void Dispose(bool disposing) + { #if DEBUG_STREAMS this.DebugDispose(typeof(SqueezeStream)); #endif - base.Dispose(disposing); + base.Dispose(disposing); + } + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => throw new NotImplementedException(); + + public override long Position + { + get => _stream.Position; + set => throw new NotImplementedException(); + } + + public override void Flush() => throw new NotImplementedException(); + + public override int Read(byte[] buffer, int offset, int count) + { + if (_processed) + { + return 0; + } + _processed = true; + using var binaryReader = new BinaryReader(_stream); + + // Read numnodes (equivalent to convert_u16!(numnodes, buf)) + var numnodes = binaryReader.ReadUInt16(); + + // Validation: numnodes should be within bounds + if (numnodes >= NUMVALS) + { + throw new InvalidDataException( + $"Invalid number of nodes {numnodes} (max {NUMVALS - 1})" + ); } - public override bool CanRead => true; - - public override bool CanSeek => false; - - public override bool CanWrite => false; - - public override long Length => throw new NotImplementedException(); - - public override long Position + // Handle the case where no nodes exist + if (numnodes == 0) { - get => _stream.Position; - set => throw new NotImplementedException(); + return 0; } - public override void Flush() => throw new NotImplementedException(); - - public override int Read(byte[] buffer, int offset, int count) + // Build dnode (tree of nodes) + var dnode = new int[numnodes, 2]; + for (int j = 0; j < numnodes; j++) { - if (_processed) + dnode[j, 0] = binaryReader.ReadInt16(); + dnode[j, 1] = binaryReader.ReadInt16(); + } + + // Initialize BitReader for reading bits + var bitReader = new BitReader(_stream); + var decoded = new List(); + + int i = 0; + // Decode the buffer using the dnode tree + while (true) + { + i = dnode[i, bitReader.ReadBit() ? 1 : 0]; + if (i < 0) { - return 0; - } - _processed = true; - using var binaryReader = new BinaryReader(_stream); - - // Read numnodes (equivalent to convert_u16!(numnodes, buf)) - var numnodes = binaryReader.ReadUInt16(); - - // Validation: numnodes should be within bounds - if (numnodes >= NUMVALS) - { - throw new InvalidDataException( - $"Invalid number of nodes {numnodes} (max {NUMVALS - 1})" - ); - } - - // Handle the case where no nodes exist - if (numnodes == 0) - { - return 0; - } - - // Build dnode (tree of nodes) - var dnode = new int[numnodes, 2]; - for (int j = 0; j < numnodes; j++) - { - dnode[j, 0] = binaryReader.ReadInt16(); - dnode[j, 1] = binaryReader.ReadInt16(); - } - - // Initialize BitReader for reading bits - var bitReader = new BitReader(_stream); - var decoded = new List(); - - int i = 0; - // Decode the buffer using the dnode tree - while (true) - { - i = dnode[i, bitReader.ReadBit() ? 1 : 0]; - if (i < 0) + i = (short)-(i + 1); + if (i == SPEOF) { - i = (short)-(i + 1); - if (i == SPEOF) - { - break; - } - else - { - decoded.Add((byte)i); - i = 0; - } + break; + } + else + { + decoded.Add((byte)i); + i = 0; } } - - // Unpack the decoded buffer using the RLE class - var unpacked = RLE.UnpackRLE(decoded.ToArray()); - unpacked.CopyTo(buffer, 0); - return unpacked.Count(); } - public override long Seek(long offset, SeekOrigin origin) => - throw new NotImplementedException(); - - public override void SetLength(long value) => throw new NotImplementedException(); - - public override void Write(byte[] buffer, int offset, int count) => - throw new NotImplementedException(); + // Unpack the decoded buffer using the RLE class + var unpacked = RLE.UnpackRLE(decoded.ToArray()); + unpacked.CopyTo(buffer, 0); + return unpacked.Count(); } -} + + public override long Seek(long offset, SeekOrigin origin) => + throw new NotImplementedException(); + + public override void SetLength(long value) => throw new NotImplementedException(); + + public override void Write(byte[] buffer, int offset, int count) => + throw new NotImplementedException(); +} \ No newline at end of file diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs index b5180afa..040263e2 100644 --- a/src/SharpCompress/Factories/ArcFactory.cs +++ b/src/SharpCompress/Factories/ArcFactory.cs @@ -10,37 +10,36 @@ using SharpCompress.Readers; using SharpCompress.Readers.Arc; using static System.Net.Mime.MediaTypeNames; -namespace SharpCompress.Factories +namespace SharpCompress.Factories; + +public class ArcFactory : Factory, IReaderFactory { - public class ArcFactory : Factory, IReaderFactory + public override string Name => "Arc"; + + public override ArchiveType? KnownArchiveType => ArchiveType.Arc; + + public override IEnumerable GetSupportedExtensions() { - public override string Name => "Arc"; - - public override ArchiveType? KnownArchiveType => ArchiveType.Arc; - - public override IEnumerable GetSupportedExtensions() - { - yield return "arc"; - } - - public override bool IsArchive( - Stream stream, - string? password = null, - int bufferSize = ReaderOptions.DefaultBufferSize - ) - { - //You may have to use some(paranoid) checks to ensure that you actually are - //processing an ARC file, since other archivers also adopted the idea of putting - //a 01Ah byte at offset 0, namely the Hyper archiver. To check if you have a - //Hyper - archive, check the next two bytes for "HP" or "ST"(or look below for - //"HYP").Also the ZOO archiver also does put a 01Ah at the start of the file, - //see the ZOO entry below. - var bytes = new byte[2]; - stream.Read(bytes, 0, 2); - return bytes[0] == 0x1A && bytes[1] < 10; //rather thin, but this is all we have - } - - public IReader OpenReader(Stream stream, ReaderOptions? options) => - ArcReader.Open(stream, options); + yield return "arc"; } -} + + public override bool IsArchive( + Stream stream, + string? password = null, + int bufferSize = ReaderOptions.DefaultBufferSize + ) + { + //You may have to use some(paranoid) checks to ensure that you actually are + //processing an ARC file, since other archivers also adopted the idea of putting + //a 01Ah byte at offset 0, namely the Hyper archiver. To check if you have a + //Hyper - archive, check the next two bytes for "HP" or "ST"(or look below for + //"HYP").Also the ZOO archiver also does put a 01Ah at the start of the file, + //see the ZOO entry below. + var bytes = new byte[2]; + stream.Read(bytes, 0, 2); + return bytes[0] == 0x1A && bytes[1] < 10; //rather thin, but this is all we have + } + + public IReader OpenReader(Stream stream, ReaderOptions? options) => + ArcReader.Open(stream, options); +} \ No newline at end of file diff --git a/src/SharpCompress/IO/IStreamStack.cs b/src/SharpCompress/IO/IStreamStack.cs index 56c020c8..2dc21ca6 100644 --- a/src/SharpCompress/IO/IStreamStack.cs +++ b/src/SharpCompress/IO/IStreamStack.cs @@ -1,45 +1,39 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; using System.IO; -using System.Linq; -using System.Text; -namespace SharpCompress.IO +namespace SharpCompress.IO; + +public interface IStreamStack { - public interface IStreamStack - { - /// - /// Gets or sets the default buffer size to be applied when buffering is enabled for this stream stack. - /// This value is used by the SetBuffer extension method to configure buffering on the appropriate stream - /// in the stack hierarchy. A value of 0 indicates no default buffer size is set. - /// - int DefaultBufferSize { get; set; } + /// + /// Gets or sets the default buffer size to be applied when buffering is enabled for this stream stack. + /// This value is used by the SetBuffer extension method to configure buffering on the appropriate stream + /// in the stack hierarchy. A value of 0 indicates no default buffer size is set. + /// + int DefaultBufferSize { get; set; } - /// - /// Returns the immediate underlying stream in the stack. - /// - Stream BaseStream(); + /// + /// Returns the immediate underlying stream in the stack. + /// + Stream BaseStream(); - /// - /// Gets or sets the size of the buffer if the stream supports buffering; otherwise, returns 0. - /// This property must not throw. - /// - int BufferSize { get; set; } + /// + /// Gets or sets the size of the buffer if the stream supports buffering; otherwise, returns 0. + /// This property must not throw. + /// + int BufferSize { get; set; } - /// - /// Gets or sets the current position within the buffer if the stream supports buffering; otherwise, returns 0. - /// This property must not throw. - /// - int BufferPosition { get; set; } + /// + /// Gets or sets the current position within the buffer if the stream supports buffering; otherwise, returns 0. + /// This property must not throw. + /// + int BufferPosition { get; set; } - /// - /// Updates the internal position state of the stream. This should not perform seeking on the underlying stream, - /// but should update any internal position or buffer state as appropriate for the stream implementation. - /// - /// The absolute position to set within the stream stack. - void SetPosition(long position); + /// + /// Updates the internal position state of the stream. This should not perform seeking on the underlying stream, + /// but should update any internal position or buffer state as appropriate for the stream implementation. + /// + /// The absolute position to set within the stream stack. + void SetPosition(long position); #if DEBUG_STREAMS /// @@ -47,318 +41,4 @@ namespace SharpCompress.IO /// long InstanceId { get; set; } #endif - } - - internal static class StackStreamExtensions - { - /// - /// Gets the logical position of the first buffering stream in the stack, or 0 if none exist. - /// - /// The most derived (outermost) stream in the stack. - /// The position of the first buffering stream, or 0 if not found. - internal static long GetPosition(this IStreamStack stream) - { - IStreamStack? current = stream; - - while (current != null) - { - if (current.BufferSize != 0 && current is Stream st) - { - return st.Position; - } - current = current?.BaseStream() as IStreamStack; - } - return 0; - } - - /// - /// Rewinds the buffer of the outermost buffering stream in the stack by the specified count, if supported. - /// Only the most derived buffering stream is affected. - /// - /// The most derived (outermost) stream in the stack. - /// The number of bytes to rewind within the buffer. - internal static void Rewind(this IStreamStack stream, int count) - { - Stream baseStream = stream.BaseStream(); - Stream thisStream = (Stream)stream; - IStreamStack? buffStream = null; - IStreamStack? current = stream; - - while (buffStream == null && current != null) - { - if (current.BufferSize != 0) - { - buffStream = current; - buffStream.BufferPosition -= Math.Min(buffStream.BufferPosition, count); - } - current = current?.BaseStream() as IStreamStack; - } - } - - /// - /// Sets the buffer size on the first buffering stream in the stack, or on the outermost stream if none exist. - /// If is true, sets the buffer size regardless of current value. - /// - /// The most derived (outermost) stream in the stack. - /// The buffer size to set. - /// If true, forces the buffer size to be set even if already set. - internal static void SetBuffer(this IStreamStack stream, int bufferSize, bool force) - { - if (bufferSize == 0 || stream == null) - return; - - IStreamStack? current = stream; - IStreamStack defaultBuffer = stream; - IStreamStack? buffer = null; - - // First pass: find the deepest IStreamStack - while (current != null) - { - defaultBuffer = current; - if (buffer == null && ((current.BufferSize != 0 && bufferSize != 0) || force)) - buffer = current; - if (defaultBuffer.DefaultBufferSize != 0) - break; - current = current.BaseStream() as IStreamStack; - } - if (defaultBuffer.DefaultBufferSize == 0) - defaultBuffer.DefaultBufferSize = bufferSize; - (buffer ?? stream).BufferSize = bufferSize; - } - - /// - /// Attempts to set the position in the stream stack. If a buffering stream is present and the position is within its buffer, - /// BufferPosition is set on the outermost buffering stream and all intermediate streams update their internal state via SetPosition. - /// If no buffering stream is present, seeks as close to the root stream as possible and updates all intermediate streams' state via SetPosition. - /// Seeking is never performed if any intermediate stream in the stack is buffering. - /// Throws if the position cannot be set. - /// - /// - /// The most derived (outermost) stream in the stack. The method traverses up the stack via BaseStream() until a stream can satisfy the buffer or seek request. - /// - /// The absolute position to set. - /// The position that was set. - internal static long StackSeek(this IStreamStack stream, long position) - { - var stack = new List(); - Stream? current = stream as Stream; - int lastBufferingIndex = -1; - int firstSeekableIndex = -1; - Stream? firstSeekableStream = null; - - // Traverse the stack, collecting info - while (current is IStreamStack stackStream) - { - stack.Add(stackStream); - if (stackStream.BufferSize > 0) - { - lastBufferingIndex = stack.Count - 1; - break; - } - current = stackStream.BaseStream(); - } - - // Find the first seekable stream (closest to the root) - if (current != null && current.CanSeek) - { - firstSeekableIndex = stack.Count; - firstSeekableStream = current; - } - - // If any buffering stream exists, try to set BufferPosition on the outermost one - if (lastBufferingIndex != -1) - { - var bufferingStream = stack[lastBufferingIndex]; - if (position >= 0 && position < bufferingStream.BufferSize) - { - bufferingStream.BufferPosition = (int)position; - return position; - } - else - { - // If position is not in buffer, reset buffer and proceed as non-buffering - bufferingStream.BufferPosition = 0; - } - // Continue to seek as if no buffer is present - } - - // If no buffering, or buffer was reset, seek at the first seekable stream (closest to the root) - if (firstSeekableStream != null) - { - firstSeekableStream.Seek(position, SeekOrigin.Begin); - return firstSeekableStream.Position; - } - - throw new NotSupportedException( - "Cannot set position on this stream stack (no seekable or buffering stream supports the requested position)." - ); - } - - /// - /// Reads bytes from the stream, using the position to observe how much was actually consumed and rewind the buffer to ensure further reads are correct. - /// This is required to prevent buffered reads from skipping data, while also benefiting from buffering and reduced stream IO reads. - /// - /// The stream to read from. - /// The buffer to read data into. - /// The offset in the buffer to start writing data. - /// The maximum number of bytes to read. - /// Returns the buffering stream found in the stack, or null if none exists. - /// Returns the number of bytes actually read from the base stream, or -1 if no buffering stream was found. - /// The number of bytes read into the buffer. - internal static int Read( - this IStreamStack stream, - byte[] buffer, - int offset, - int count, - out IStreamStack? buffStream, - out int baseReadCount - ) - { - Stream baseStream = stream.BaseStream(); - Stream thisStream = (Stream)stream; - IStreamStack? current = stream; - buffStream = null; - baseReadCount = -1; - - while (buffStream == null && (current = current?.BaseStream() as IStreamStack) != null) - { - if (current.BufferSize != 0) - { - buffStream = current; - } - } - - long buffPos = buffStream == null ? -1 : ((Stream)buffStream).Position; - - int read = baseStream.Read(buffer, offset, count); //amount read in to buffer - - if (buffPos != -1) - { - baseReadCount = (int)(((Stream)buffStream!).Position - buffPos); - } - return read; - } - -#if DEBUG_STREAMS - private static long _instanceCounter = 0; - - private static string cleansePos(long pos) - { - if (pos < 0) - return ""; - return "Px" + pos.ToString("x"); - } - - /// - /// Gets or creates a unique instance ID for the stream stack for debugging purposes. - /// - /// The stream stack. - /// Reference to the instance ID field. - /// Whether this is being called during construction. - /// The instance ID. - public static long GetInstanceId( - this IStreamStack stream, - ref long instanceId, - bool construct - ) - { - if (instanceId == 0) //will not be equal to 0 when inherited IStackStream types are being used - instanceId = System.Threading.Interlocked.Increment(ref _instanceCounter); - return instanceId; - } - - /// - /// Writes a debug message for stream construction. - /// - /// The stream stack. - /// The type being constructed. - public static void DebugConstruct(this IStreamStack stream, Type constructing) - { - long id = stream.InstanceId; - stream.InstanceId = GetInstanceId(stream, ref id, true); - var frame = (new StackTrace()).GetFrame(3); - string parentInfo = - frame != null - ? $"{frame.GetMethod()?.DeclaringType?.Name}.{frame.GetMethod()?.Name}()" - : "Unknown"; - if (constructing.FullName == stream.GetType().FullName) //don't debug base IStackStream types - Debug.WriteLine( - $"{GetStreamStackString(stream, true)} : Constructed by [{parentInfo}]" - ); - } - - /// - /// Writes a debug message for stream disposal. - /// - /// The stream stack. - /// The type being disposed. - public static void DebugDispose(this IStreamStack stream, Type constructing) - { - var frame = (new StackTrace()).GetFrame(3); - string parentInfo = - frame != null - ? $"{frame.GetMethod()?.DeclaringType?.Name}.{frame.GetMethod()?.Name}()" - : "Unknown"; - if (constructing.FullName == stream.GetType().FullName) //don't debug base IStackStream types - Debug.WriteLine($"{GetStreamStackString(stream, false)} : Disposed by [{parentInfo}]"); - } - - /// - /// Writes a debug trace message for the stream. - /// - /// The stream stack. - /// The debug message to write. - public static void DebugTrace(this IStreamStack stream, string message) - { - Debug.WriteLine( - $"{GetStreamStackString(stream, false)} : [{stream.GetType().Name}]{message}" - ); - } - - /// - /// Returns the full stream chain as a string, including instance IDs and positions. - /// - /// The stream stack to represent. - /// Whether this is being called during construction. - /// A string representation of the entire stream stack. - public static string GetStreamStackString(this IStreamStack stream, bool construct) - { - var sb = new StringBuilder(); - Stream? current = stream as Stream; - while (current != null) - { - IStreamStack? sStack = current as IStreamStack; - string id = sStack != null ? "#" + sStack.InstanceId.ToString() : ""; - string buffSize = sStack != null ? "Bx" + sStack.BufferSize.ToString("x") : ""; - string defBuffSize = - sStack != null ? "Dx" + sStack.DefaultBufferSize.ToString("x") : ""; - - if (sb.Length > 0) - sb.Insert(0, "/"); - try - { - sb.Insert( - 0, - $"{current.GetType().Name}{id}[{cleansePos(current.Position)}:{buffSize}:{defBuffSize}]" - ); - } - catch - { - if (current is SharpCompressStream scs) - sb.Insert( - 0, - $"{current.GetType().Name}{id}[{cleansePos(scs.InternalPosition)}:{buffSize}:{defBuffSize}]" - ); - else - sb.Insert(0, $"{current.GetType().Name}{id}[:{buffSize}]"); - } - if (sStack != null) - current = sStack.BaseStream(); //current may not be a IStreamStack, allow one more loop - else - break; - } - return sb.ToString(); - } -#endif - } } diff --git a/src/SharpCompress/IO/StackStreamExtensions.cs b/src/SharpCompress/IO/StackStreamExtensions.cs new file mode 100644 index 00000000..4d29c23e --- /dev/null +++ b/src/SharpCompress/IO/StackStreamExtensions.cs @@ -0,0 +1,320 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace SharpCompress.IO; + +internal static class StackStreamExtensions +{ + /// + /// Gets the logical position of the first buffering stream in the stack, or 0 if none exist. + /// + /// The most derived (outermost) stream in the stack. + /// The position of the first buffering stream, or 0 if not found. + internal static long GetPosition(this IStreamStack stream) + { + IStreamStack? current = stream; + + while (current != null) + { + if (current.BufferSize != 0 && current is Stream st) + { + return st.Position; + } + current = current?.BaseStream() as IStreamStack; + } + return 0; + } + + /// + /// Rewinds the buffer of the outermost buffering stream in the stack by the specified count, if supported. + /// Only the most derived buffering stream is affected. + /// + /// The most derived (outermost) stream in the stack. + /// The number of bytes to rewind within the buffer. + internal static void Rewind(this IStreamStack stream, int count) + { + Stream baseStream = stream.BaseStream(); + Stream thisStream = (Stream)stream; + IStreamStack? buffStream = null; + IStreamStack? current = stream; + + while (buffStream == null && current != null) + { + if (current.BufferSize != 0) + { + buffStream = current; + buffStream.BufferPosition -= Math.Min(buffStream.BufferPosition, count); + } + current = current?.BaseStream() as IStreamStack; + } + } + + /// + /// Sets the buffer size on the first buffering stream in the stack, or on the outermost stream if none exist. + /// If is true, sets the buffer size regardless of current value. + /// + /// The most derived (outermost) stream in the stack. + /// The buffer size to set. + /// If true, forces the buffer size to be set even if already set. + internal static void SetBuffer(this IStreamStack stream, int bufferSize, bool force) + { + if (bufferSize == 0 || stream == null) + return; + + IStreamStack? current = stream; + IStreamStack defaultBuffer = stream; + IStreamStack? buffer = null; + + // First pass: find the deepest IStreamStack + while (current != null) + { + defaultBuffer = current; + if (buffer == null && ((current.BufferSize != 0 && bufferSize != 0) || force)) + buffer = current; + if (defaultBuffer.DefaultBufferSize != 0) + break; + current = current.BaseStream() as IStreamStack; + } + if (defaultBuffer.DefaultBufferSize == 0) + defaultBuffer.DefaultBufferSize = bufferSize; + (buffer ?? stream).BufferSize = bufferSize; + } + + /// + /// Attempts to set the position in the stream stack. If a buffering stream is present and the position is within its buffer, + /// BufferPosition is set on the outermost buffering stream and all intermediate streams update their internal state via SetPosition. + /// If no buffering stream is present, seeks as close to the root stream as possible and updates all intermediate streams' state via SetPosition. + /// Seeking is never performed if any intermediate stream in the stack is buffering. + /// Throws if the position cannot be set. + /// + /// + /// The most derived (outermost) stream in the stack. The method traverses up the stack via BaseStream() until a stream can satisfy the buffer or seek request. + /// + /// The absolute position to set. + /// The position that was set. + internal static long StackSeek(this IStreamStack stream, long position) + { + var stack = new List(); + Stream? current = stream as Stream; + int lastBufferingIndex = -1; + int firstSeekableIndex = -1; + Stream? firstSeekableStream = null; + + // Traverse the stack, collecting info + while (current is IStreamStack stackStream) + { + stack.Add(stackStream); + if (stackStream.BufferSize > 0) + { + lastBufferingIndex = stack.Count - 1; + break; + } + current = stackStream.BaseStream(); + } + + // Find the first seekable stream (closest to the root) + if (current != null && current.CanSeek) + { + firstSeekableIndex = stack.Count; + firstSeekableStream = current; + } + + // If any buffering stream exists, try to set BufferPosition on the outermost one + if (lastBufferingIndex != -1) + { + var bufferingStream = stack[lastBufferingIndex]; + if (position >= 0 && position < bufferingStream.BufferSize) + { + bufferingStream.BufferPosition = (int)position; + return position; + } + else + { + // If position is not in buffer, reset buffer and proceed as non-buffering + bufferingStream.BufferPosition = 0; + } + // Continue to seek as if no buffer is present + } + + // If no buffering, or buffer was reset, seek at the first seekable stream (closest to the root) + if (firstSeekableStream != null) + { + firstSeekableStream.Seek(position, SeekOrigin.Begin); + return firstSeekableStream.Position; + } + + throw new NotSupportedException( + "Cannot set position on this stream stack (no seekable or buffering stream supports the requested position)." + ); + } + + /// + /// Reads bytes from the stream, using the position to observe how much was actually consumed and rewind the buffer to ensure further reads are correct. + /// This is required to prevent buffered reads from skipping data, while also benefiting from buffering and reduced stream IO reads. + /// + /// The stream to read from. + /// The buffer to read data into. + /// The offset in the buffer to start writing data. + /// The maximum number of bytes to read. + /// Returns the buffering stream found in the stack, or null if none exists. + /// Returns the number of bytes actually read from the base stream, or -1 if no buffering stream was found. + /// The number of bytes read into the buffer. + internal static int Read( + this IStreamStack stream, + byte[] buffer, + int offset, + int count, + out IStreamStack? buffStream, + out int baseReadCount + ) + { + Stream baseStream = stream.BaseStream(); + Stream thisStream = (Stream)stream; + IStreamStack? current = stream; + buffStream = null; + baseReadCount = -1; + + while (buffStream == null && (current = current?.BaseStream() as IStreamStack) != null) + { + if (current.BufferSize != 0) + { + buffStream = current; + } + } + + long buffPos = buffStream == null ? -1 : ((Stream)buffStream).Position; + + int read = baseStream.Read(buffer, offset, count); //amount read in to buffer + + if (buffPos != -1) + { + baseReadCount = (int)(((Stream)buffStream!).Position - buffPos); + } + return read; + } + +#if DEBUG_STREAMS + private static long _instanceCounter = 0; + + private static string cleansePos(long pos) + { + if (pos < 0) + return ""; + return "Px" + pos.ToString("x"); + } + + /// + /// Gets or creates a unique instance ID for the stream stack for debugging purposes. + /// + /// The stream stack. + /// Reference to the instance ID field. + /// Whether this is being called during construction. + /// The instance ID. + public static long GetInstanceId( + this IStreamStack stream, + ref long instanceId, + bool construct + ) + { + if (instanceId == 0) //will not be equal to 0 when inherited IStackStream types are being used + instanceId = System.Threading.Interlocked.Increment(ref _instanceCounter); + return instanceId; + } + + /// + /// Writes a debug message for stream construction. + /// + /// The stream stack. + /// The type being constructed. + public static void DebugConstruct(this IStreamStack stream, Type constructing) + { + long id = stream.InstanceId; + stream.InstanceId = GetInstanceId(stream, ref id, true); + var frame = (new StackTrace()).GetFrame(3); + string parentInfo = + frame != null + ? $"{frame.GetMethod()?.DeclaringType?.Name}.{frame.GetMethod()?.Name}()" + : "Unknown"; + if (constructing.FullName == stream.GetType().FullName) //don't debug base IStackStream types + Debug.WriteLine( + $"{GetStreamStackString(stream, true)} : Constructed by [{parentInfo}]" + ); + } + + /// + /// Writes a debug message for stream disposal. + /// + /// The stream stack. + /// The type being disposed. + public static void DebugDispose(this IStreamStack stream, Type constructing) + { + var frame = (new StackTrace()).GetFrame(3); + string parentInfo = + frame != null + ? $"{frame.GetMethod()?.DeclaringType?.Name}.{frame.GetMethod()?.Name}()" + : "Unknown"; + if (constructing.FullName == stream.GetType().FullName) //don't debug base IStackStream types + Debug.WriteLine($"{GetStreamStackString(stream, false)} : Disposed by [{parentInfo}]"); + } + + /// + /// Writes a debug trace message for the stream. + /// + /// The stream stack. + /// The debug message to write. + public static void DebugTrace(this IStreamStack stream, string message) + { + Debug.WriteLine( + $"{GetStreamStackString(stream, false)} : [{stream.GetType().Name}]{message}" + ); + } + + /// + /// Returns the full stream chain as a string, including instance IDs and positions. + /// + /// The stream stack to represent. + /// Whether this is being called during construction. + /// A string representation of the entire stream stack. + public static string GetStreamStackString(this IStreamStack stream, bool construct) + { + var sb = new StringBuilder(); + Stream? current = stream as Stream; + while (current != null) + { + IStreamStack? sStack = current as IStreamStack; + string id = sStack != null ? "#" + sStack.InstanceId.ToString() : ""; + string buffSize = sStack != null ? "Bx" + sStack.BufferSize.ToString("x") : ""; + string defBuffSize = + sStack != null ? "Dx" + sStack.DefaultBufferSize.ToString("x") : ""; + + if (sb.Length > 0) + sb.Insert(0, "/"); + try + { + sb.Insert( + 0, + $"{current.GetType().Name}{id}[{cleansePos(current.Position)}:{buffSize}:{defBuffSize}]" + ); + } + catch + { + if (current is SharpCompressStream scs) + sb.Insert( + 0, + $"{current.GetType().Name}{id}[{cleansePos(scs.InternalPosition)}:{buffSize}:{defBuffSize}]" + ); + else + sb.Insert(0, $"{current.GetType().Name}{id}[:{buffSize}]"); + } + if (sStack != null) + current = sStack.BaseStream(); //current may not be a IStreamStack, allow one more loop + else + break; + } + return sb.ToString(); + } +#endif +} diff --git a/src/SharpCompress/Readers/Arc/ArcReader.cs b/src/SharpCompress/Readers/Arc/ArcReader.cs index 7d58b8d4..6ad6a382 100644 --- a/src/SharpCompress/Readers/Arc/ArcReader.cs +++ b/src/SharpCompress/Readers/Arc/ArcReader.cs @@ -7,35 +7,34 @@ using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Arc; -namespace SharpCompress.Readers.Arc +namespace SharpCompress.Readers.Arc; + +public class ArcReader : AbstractReader { - public class ArcReader : AbstractReader + private ArcReader(Stream stream, ReaderOptions options) + : base(options, ArchiveType.Arc) => Volume = new ArcVolume(stream, options, 0); + + public override ArcVolume Volume { get; } + + /// + /// Opens an ArcReader for Non-seeking usage with a single volume + /// + /// + /// + /// + public static ArcReader Open(Stream stream, ReaderOptions? options = null) { - private ArcReader(Stream stream, ReaderOptions options) - : base(options, ArchiveType.Arc) => Volume = new ArcVolume(stream, options, 0); + stream.CheckNotNull(nameof(stream)); + return new ArcReader(stream, options ?? new ReaderOptions()); + } - public override ArcVolume Volume { get; } - - /// - /// Opens an ArcReader for Non-seeking usage with a single volume - /// - /// - /// - /// - public static ArcReader Open(Stream stream, ReaderOptions? options = null) + protected override IEnumerable GetEntries(Stream stream) + { + ArcEntryHeader headerReader = new ArcEntryHeader(new ArchiveEncoding()); + ArcEntryHeader? header; + while ((header = headerReader.ReadHeader(stream)) != null) { - stream.CheckNotNull(nameof(stream)); - return new ArcReader(stream, options ?? new ReaderOptions()); - } - - protected override IEnumerable GetEntries(Stream stream) - { - ArcEntryHeader headerReader = new ArcEntryHeader(new ArchiveEncoding()); - ArcEntryHeader? header; - while ((header = headerReader.ReadHeader(stream)) != null) - { - yield return new ArcEntry(new ArcFilePart(header, stream)); - } + yield return new ArcEntry(new ArcFilePart(header, stream)); } } -} +} \ No newline at end of file