diff --git a/src/SharpCompress/Common/Arc/ArcEntryHeader.cs b/src/SharpCompress/Common/Arc/ArcEntryHeader.cs index 01243c92..824d90eb 100644 --- a/src/SharpCompress/Common/Arc/ArcEntryHeader.cs +++ b/src/SharpCompress/Common/Arc/ArcEntryHeader.cs @@ -28,6 +28,7 @@ namespace SharpCompress.Common.Arc { return null; } + DataStartPosition = stream.Position; return LoadFrom(headerBytes); } @@ -56,8 +57,8 @@ namespace SharpCompress.Common.Arc return value switch { 1 or 2 => CompressionType.None, - //3 => CompressionType.RLE90, - //4 => CompressionType.Squeezed, + 3 => CompressionType.RLE90, + 4 => CompressionType.Squeezed, //5 or 6 or 7 or 8 => CompressionType.Crunched, //9 => CompressionType.Squashed, //10 => CompressionType.Crushed, diff --git a/src/SharpCompress/Common/Arc/ArcFilePart.cs b/src/SharpCompress/Common/Arc/ArcFilePart.cs index 7796e5e8..1308d658 100644 --- a/src/SharpCompress/Common/Arc/ArcFilePart.cs +++ b/src/SharpCompress/Common/Arc/ArcFilePart.cs @@ -8,6 +8,8 @@ using SharpCompress.Common.GZip; using SharpCompress.Common.Tar; using SharpCompress.Common.Tar.Headers; using SharpCompress.Common.Zip.Headers; +using SharpCompress.Compressors.RLE90; +using SharpCompress.Compressors.Squeezed; using SharpCompress.IO; namespace SharpCompress.Common.Arc @@ -31,7 +33,31 @@ namespace SharpCompress.Common.Arc { if (_stream != null) { - return new ReadOnlySubStream(_stream, Header.CompressedSize); + 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; + default: + throw new NotSupportedException( + "CompressionMethod: " + Header.CompressionMethod + ); + } + return compressedStream; } return _stream.NotNull(); } diff --git a/src/SharpCompress/Common/CompressionType.cs b/src/SharpCompress/Common/CompressionType.cs index 45b6aeeb..14ec5a1a 100644 --- a/src/SharpCompress/Common/CompressionType.cs +++ b/src/SharpCompress/Common/CompressionType.cs @@ -22,4 +22,6 @@ public enum CompressionType Reduce3, Reduce4, Explode, + Squeezed, + RLE90, } diff --git a/src/SharpCompress/Compressors/RLE90/RunLength90Stream.cs b/src/SharpCompress/Compressors/RLE90/RunLength90Stream.cs new file mode 100644 index 00000000..cdc4ce1f --- /dev/null +++ b/src/SharpCompress/Compressors/RLE90/RunLength90Stream.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SharpCompress.Compressors.RLE90 +{ + public class RunLength90Stream : Stream + { + 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; + } + + 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); + + int bufferIndex = offset; + bool inCountState = false; + byte last = 0; + + foreach (byte c in compressedBuffer) + { + if (inCountState) + { + if (c == 0) + { + if (bufferIndex < buffer.Length) + buffer[bufferIndex++] = DLE; + } + else + { + int repeatCount = Math.Min(c - 1, buffer.Length - bufferIndex); + for (int i = 0; i < repeatCount; i++) + buffer[bufferIndex++] = last; + } + inCountState = false; + } + else + { + if (c == DLE) + { + inCountState = true; + } + else + { + if (bufferIndex < buffer.Length) + buffer[bufferIndex++] = c; + last = c; + } + } + + if (bufferIndex >= offset + count) + break; // Stop when we fill the requested buffer size + } + + return bufferIndex - offset; + } + + 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(); + } +} diff --git a/src/SharpCompress/Compressors/Squeezed/BitReader.cs b/src/SharpCompress/Compressors/Squeezed/BitReader.cs new file mode 100644 index 00000000..bc8920df --- /dev/null +++ b/src/SharpCompress/Compressors/Squeezed/BitReader.cs @@ -0,0 +1,36 @@ +using System.IO; + +namespace SharpCompress.Compressors.Squeezed +{ + // Helper BitReader class for reading individual bits + public class BitReader + { + private readonly Stream _stream; + private int _bitBuffer; + private int _bitCount; + + public BitReader(Stream stream) + { + _stream = stream; + _bitBuffer = 0; + _bitCount = 0; + } + + public bool ReadBit() + { + if (_bitCount == 0) + { + int nextByte = _stream.ReadByte(); + if (nextByte == -1) + throw new EndOfStreamException(); + _bitBuffer = nextByte; + _bitCount = 8; + } + + bool bit = (_bitBuffer & 1) != 0; + _bitBuffer >>= 1; + _bitCount--; + return bit; + } + } +} diff --git a/src/SharpCompress/Compressors/Squeezed/RLE.cs b/src/SharpCompress/Compressors/Squeezed/RLE.cs new file mode 100644 index 00000000..5eda6432 --- /dev/null +++ b/src/SharpCompress/Compressors/Squeezed/RLE.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; + +namespace SharpCompress.Compressors.Squeezed +{ + 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) + { + var result = new List(compressedBuffer.Length * 2); // Optimized initial capacity + bool countMode = false; + byte last = 0; + + foreach (var c in compressedBuffer) + { + if (!countMode) + { + if (c == DLE) + { + countMode = true; + } + else + { + result.Add(c); + last = c; + } + } + else + { + countMode = false; + if (c == 0) + { + result.Add(DLE); + } + else + { + result.AddRange(Enumerable.Repeat(last, c - 1)); + } + } + } + + return result; + } + } +} diff --git a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs new file mode 100644 index 00000000..f48ad449 --- /dev/null +++ b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using ZstdSharp.Unsafe; + +namespace SharpCompress.Compressors.Squeezed +{ + public class SqueezeStream : Stream + { + 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 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})" + ); + } + + // 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) + { + 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(); + } +} diff --git a/tests/SharpCompress.Test/Arc/ArcReaderTests.cs b/tests/SharpCompress.Test/Arc/ArcReaderTests.cs index 45efe754..c23687cd 100644 --- a/tests/SharpCompress.Test/Arc/ArcReaderTests.cs +++ b/tests/SharpCompress.Test/Arc/ArcReaderTests.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using SharpCompress.Common; +using SharpCompress.Readers; +using SharpCompress.Readers.Arc; using Xunit; namespace SharpCompress.Test.Arc @@ -18,5 +21,28 @@ namespace SharpCompress.Test.Arc [Fact] public void Arc_Uncompressed_Read() => Read("Arc.uncompressed.arc", CompressionType.None); + + [Fact] + public void Arc_SqueezedAndPacked_Read() + { + //archive contains two different compression methods, hence the need for a specific test function + using ( + Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Arc.squeezed.arc")) + ) + using (IReader reader = ArcReader.Open(stream)) + { + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + reader.WriteEntryToDirectory( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + } + } + } + VerifyFilesByExtension(); + } } } diff --git a/tests/TestArchives/Archives/Arc.squeezed.arc b/tests/TestArchives/Archives/Arc.squeezed.arc new file mode 100644 index 00000000..cba07e30 Binary files /dev/null and b/tests/TestArchives/Archives/Arc.squeezed.arc differ