Implementation of squeezed and packed compression algorithms for .ARC archive format

This commit is contained in:
Twan van Dongen
2025-03-11 18:15:53 +01:00
parent 825c61bdcd
commit eaf466c5c3
9 changed files with 360 additions and 3 deletions

View File

@@ -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,

View File

@@ -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();
}

View File

@@ -22,4 +22,6 @@ public enum CompressionType
Reduce3,
Reduce4,
Explode,
Squeezed,
RLE90,
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Linq;
namespace SharpCompress.Compressors.Squeezed
{
public static class RLE
{
private const byte DLE = 0x90;
/// <summary>
/// Unpacks an RLE compressed buffer.
/// Format: <char> DLE <count>, where count == 0 -> DLE
/// </summary>
/// <param name="compressedBuffer">The compressed buffer to unpack.</param>
/// <returns>A list of unpacked bytes.</returns>
public static List<byte> UnpackRLE(byte[] compressedBuffer)
{
var result = new List<byte>(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;
}
}
}

View File

@@ -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<byte>();
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();
}
}

View File

@@ -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();
}
}
}

Binary file not shown.