diff --git a/lib/AuroraLip/Compression/FlagReader.cs b/lib/AuroraLip/Compression/FlagReader.cs new file mode 100644 index 00000000..7b33d984 --- /dev/null +++ b/lib/AuroraLip/Compression/FlagReader.cs @@ -0,0 +1,67 @@ +using AuroraLib.Common; + +namespace AuroraLip.Compression +{ + /// + /// Reads individual bits from a stream and provides methods for interpreting the flag values. + /// + public class FlagReader + { + private byte CurrentFlag; + public byte BitsLeft { get; private set; } + public readonly Stream Base; + public readonly Endian Order; + + public FlagReader(Stream source, Endian order) + { + Base = source; + Order = order; + } + + /// + /// Reads a single bit from the stream. + /// + /// The value of the read bit. + public bool Readbit() + { + if (BitsLeft == 0) + { + CurrentFlag = Base.ReadUInt8(); + BitsLeft = 8; + } + + bool flag; + if (Order == Endian.Little) + { + flag = (CurrentFlag & 1) == 1; + CurrentFlag >>= 1; + } + else + { + flag = (CurrentFlag & 128) == 128; + CurrentFlag <<= 1; + } + BitsLeft--; + return flag; + } + + /// + /// Reads an integer value with the specified number of bits from the stream. + /// + /// The number of bits to read. + /// The integer value read from the stream. + public int ReadInt(int bits = 1) + { + int vaule = 0; + for (int i = 0; i < bits; i++) + { + vaule <<= 1; + if (Readbit()) + { + vaule |= 1; + } + } + return vaule; + } + } +} diff --git a/lib/AuroraLip/Compression/FlagWriter.cs b/lib/AuroraLip/Compression/FlagWriter.cs new file mode 100644 index 00000000..5271d4d3 --- /dev/null +++ b/lib/AuroraLip/Compression/FlagWriter.cs @@ -0,0 +1,84 @@ +using AuroraLib.Common; + +namespace AuroraLib.Compression +{ + /// + /// Represents a flag writer used for compressing data. It provides methods to write individual bits. + /// + public class FlagWriter + { + private byte CurrentFlag; + public byte BitsLeft { get; private set; } + public readonly Stream Base; + public readonly MemoryStream Buffer; + public readonly Endian Order; + + public FlagWriter(Stream destination, MemoryStream buffer, Endian order) + { + Base = destination; + Order = order; + Buffer = buffer; + } + + /// + /// Writes a single bit as a flag. The bits are accumulated in a byte and flushed to the destination stream when necessary. + /// + /// The bit value to write (true for 1, false for 0). + public void WriteBit(bool bit) + { + if (BitsLeft == 0) + { + CurrentFlag = 0; + BitsLeft = 8; + } + + if (bit) + { + if (Order == Endian.Little) + CurrentFlag |= (byte)(1 << (8 - BitsLeft)); + else + CurrentFlag |= (byte)(1 << (BitsLeft - 1)); + } + + BitsLeft--; + + if (BitsLeft == 0) + { + Base.WriteByte(CurrentFlag); + Buffer.WriteTo(Base); + Buffer.SetLength(0); + } + } + + /// + /// Writes an integer value as a sequence of bits with the specified number of bits. The bits are written from the most significant bit to the least significant bit. + /// + /// The integer value to write. + /// The number of bits to write (default is 1). + public void WriteInt(int value, int bits = 1) + { + for (int i = bits - 1; i >= 0; i--) + { + int bit = (value >> i) & 1; + WriteBit(bit == 1); + } + } + + /// + /// Flushes any remaining bits in the buffer to the underlying stream. + /// + public void Flush() + { + if (BitsLeft != 0) + { + Base.WriteByte(CurrentFlag); + BitsLeft = 0; + } + if (Buffer.Length != 0) + { + Buffer.WriteTo(Base); + Buffer.SetLength(0); + } + } + } +} diff --git a/lib/AuroraLip/Compression/Formats/PRS.cs b/lib/AuroraLip/Compression/Formats/PRS.cs index 883ff5b1..6f3fca7e 100644 --- a/lib/AuroraLip/Compression/Formats/PRS.cs +++ b/lib/AuroraLip/Compression/Formats/PRS.cs @@ -1,4 +1,5 @@ using AuroraLib.Common; +using AuroraLip.Compression; namespace AuroraLib.Compression.Formats { @@ -12,45 +13,53 @@ namespace AuroraLib.Compression.Formats /// /// The PRS compression algorithm is based on LZ77 with run-length encoding emulation and extended matches. /// - public class PRS : ICompression + public partial class PRS : ICompression { public bool CanRead => true; public bool CanWrite => true; + public static Endian Order => Endian.Little; + public bool IsMatch(Stream stream, in string extension = "") - => stream.Length > 3 && (stream.ReadByte() & 0x1) == 1 && stream.At(-2, SeekOrigin.End, S => S.ReadUInt16()) == 0; + => stream.Length > 6 & stream.PeekByte() > 17 && (stream.ReadByte() & 0x1) == 1 && stream.At(-2, SeekOrigin.End, S => S.ReadUInt16()) == 0; public byte[] Decompress(Stream source) { - int bitPos = 9; - byte currentByte; + return Decompress_ALG(source, Order); + } + + public void Compress(in byte[] source, Stream destination) + => Compress_ALG(source, destination, Order); + + public static byte[] Decompress_ALG(Stream source, Endian order = Endian.Little) + { int lookBehindOffset, lookBehindLength; Stream destination = new MemoryStream(); + FlagReader Flag = new(source, order); - currentByte = source.ReadUInt8(); - while (true) + while (source.Position < source.Length) { - if (GetControlBit(ref bitPos, ref currentByte, source) != 0) + if (Flag.Readbit()) // Uncompressed value { - // Direct byte destination.WriteByte(source.ReadUInt8()); continue; } - if (GetControlBit(ref bitPos, ref currentByte, source) != 0) + if (Flag.Readbit()) // Compressed value { - lookBehindOffset = source.ReadUInt8(); - lookBehindOffset |= source.ReadUInt8() << 8; - if (lookBehindOffset == 0) + // Long + lookBehindOffset = source.ReadUInt16(order); + + if (lookBehindOffset == 0 && source.Position < source.Length) { - // End of the compressed data break; } lookBehindLength = lookBehindOffset & 7; - lookBehindOffset = (lookBehindOffset >> 3) | -0x2000; + lookBehindOffset >>= 3; + lookBehindOffset |= -0x2000; if (lookBehindLength == 0) { lookBehindLength = source.ReadUInt8() + 1; @@ -62,73 +71,47 @@ public byte[] Decompress(Stream source) } else { - lookBehindLength = 0; - lookBehindLength = (lookBehindLength << 1) | GetControlBit(ref bitPos, ref currentByte, source); - lookBehindLength = (lookBehindLength << 1) | GetControlBit(ref bitPos, ref currentByte, source); - lookBehindOffset = source.ReadUInt8() | -0x100; - lookBehindLength += 2; + // Short + lookBehindLength = Flag.ReadInt(2) + 2; + lookBehindOffset = source.ReadUInt8(); + lookBehindOffset |= -0x100; } for (int i = 0; i < lookBehindLength; i++) { - long writePosition = destination.Position; - destination.Seek(writePosition + lookBehindOffset, SeekOrigin.Begin); - byte b = destination.ReadUInt8(); - destination.Seek(writePosition, SeekOrigin.Begin); - destination.WriteByte(b); + destination.WriteByte(destination.At(lookBehindOffset, SeekOrigin.Current, s => s.ReadUInt8())); } } return destination.ToArray(); } - public void Compress(in byte[] Data, Stream destination) + public static void Compress_ALG(in byte[] source, Stream destination, Endian order = Endian.Little) { - Stream source = new MemoryStream(Data); - - // Get the source length - int sourceLength = (int)(source.Length - source.Position); - - byte[] sourceArray = new byte[sourceLength]; - var totalSourceBytesRead = 0; - int sourceBytesRead; - do - { - sourceBytesRead = source.Read(sourceArray, totalSourceBytesRead, sourceLength - totalSourceBytesRead); - if (sourceBytesRead == 0) - { - throw new IOException($"Unable to read all bytes in {nameof(source)}"); - } - totalSourceBytesRead += sourceBytesRead; - } - while (totalSourceBytesRead < sourceLength); - - byte bitPos = 0; - byte controlByte = 0; - int position = 0; int currentLookBehindPosition, currentLookBehindLength; int lookBehindOffset, lookBehindLength; - MemoryStream data = new MemoryStream(); + MemoryStream buffer = new(); + FlagWriter flagWriter = new(destination, buffer, order); - while (position < sourceLength) + while (position < source.Length) { - currentLookBehindLength = 0; lookBehindOffset = 0; lookBehindLength = 0; for (currentLookBehindPosition = position - 1; (currentLookBehindPosition >= 0) && (currentLookBehindPosition >= position - 0x1FF0) && (lookBehindLength < 256); currentLookBehindPosition--) { currentLookBehindLength = 1; - if (sourceArray[currentLookBehindPosition] == sourceArray[position]) + if (source[currentLookBehindPosition] == source[position]) { do { currentLookBehindLength++; - } while ((currentLookBehindLength <= 256) && - (position + currentLookBehindLength <= sourceArray.Length) && - sourceArray[currentLookBehindPosition + currentLookBehindLength - 1] == sourceArray[position + currentLookBehindLength - 1]); + } + while ((currentLookBehindLength <= 256) && + (position + currentLookBehindLength <= source.Length) && + source[currentLookBehindPosition + currentLookBehindLength - 1] == source[position + currentLookBehindLength - 1]); currentLookBehindLength--; if (((currentLookBehindLength >= 2 && currentLookBehindPosition - position >= -0x100) || currentLookBehindLength >= 3) && currentLookBehindLength > lookBehindLength) @@ -139,96 +122,52 @@ public void Compress(in byte[] Data, Stream destination) } } - if (lookBehindLength == 0) + if (lookBehindLength == 0) // Uncompressed value { - data.WriteByte(sourceArray[position++]); - PutControlBit(1, ref controlByte, ref bitPos, data, destination); + buffer.WriteByte(source[position++]); + flagWriter.WriteBit(true); } - else + else // Compressed value { - Copy(lookBehindOffset, lookBehindLength, ref controlByte, ref bitPos, data, destination); position += lookBehindLength; + flagWriter.WriteBit(false); + if ((lookBehindOffset >= -0x100) && (lookBehindLength <= 5)) + { + // Short + flagWriter.WriteBit(false); + lookBehindLength -= 2; + flagWriter.WriteBit(((lookBehindLength >> 1) & 1) == 1); + buffer.WriteByte((byte)(lookBehindOffset & 0xFF)); + flagWriter.WriteBit((lookBehindLength & 1) == 1); + } + else + { + // Long + if (lookBehindLength <= 9) + { + lookBehindLength -= 2; + ushort value = (ushort)((lookBehindOffset << 3) | (lookBehindLength & 0x07)); + buffer.Write(value, order); + } + else + { + ushort value = (ushort)((lookBehindOffset << 3)); + buffer.Write(value, order); + buffer.WriteByte((byte)(lookBehindLength - 1)); + } + flagWriter.WriteBit(true); + } } } - PutControlBit(0, ref controlByte, ref bitPos, data, destination); - PutControlBit(1, ref controlByte, ref bitPos, data, destination); - if (bitPos != 0) - { - controlByte = (byte)((controlByte << bitPos) >> 8); - Flush(ref controlByte, ref bitPos, data, destination); - } + flagWriter.WriteBit(false); + flagWriter.WriteBit(true); + flagWriter.Flush(); destination.WriteByte(0); destination.WriteByte(0); return; } - private static void Copy(int offset, int size, ref byte controlByte, ref byte bitPos, MemoryStream data, Stream destination) - { - if ((offset >= -0x100) && (size <= 5)) - { - size -= 2; - PutControlBit(0, ref controlByte, ref bitPos, data, destination); - PutControlBit(0, ref controlByte, ref bitPos, data, destination); - PutControlBit((size >> 1) & 1, ref controlByte, ref bitPos, data, destination); - data.WriteByte((byte)(offset & 0xFF)); - PutControlBit(size & 1, ref controlByte, ref bitPos, data, destination); - } - else - { - if (size <= 9) - { - PutControlBit(0, ref controlByte, ref bitPos, data, destination); - data.WriteByte((byte)(((offset << 3) & 0xF8) | ((size - 2) & 0x07))); - data.WriteByte((byte)((offset >> 5) & 0xFF)); - PutControlBit(1, ref controlByte, ref bitPos, data, destination); - } - else - { - PutControlBit(0, ref controlByte, ref bitPos, data, destination); - data.WriteByte((byte)((offset << 3) & 0xF8)); - data.WriteByte((byte)((offset >> 5) & 0xFF)); - data.WriteByte((byte)(size - 1)); - PutControlBit(1, ref controlByte, ref bitPos, data, destination); - } - } - } - - private static void PutControlBit(int bit, ref byte controlByte, ref byte bitPos, MemoryStream data, Stream destination) - { - controlByte >>= 1; - controlByte |= (byte)(bit << 7); - bitPos++; - if (bitPos >= 8) - { - Flush(ref controlByte, ref bitPos, data, destination); - } - } - - private static void Flush(ref byte controlByte, ref byte bitPos, MemoryStream data, Stream destination) - { - destination.WriteByte(controlByte); - controlByte = 0; - bitPos = 0; - - byte[] bytes = data.ToArray(); - destination.Write(bytes, 0, bytes.Length); - data.SetLength(0); - } - - private static int GetControlBit(ref int bitPos, ref byte currentByte, Stream source) - { - bitPos--; - if (bitPos == 0) - { - currentByte = source.ReadUInt8(); - bitPos = 8; - } - - int flag = currentByte & 1; - currentByte >>= 1; - return flag; - } } } diff --git a/lib/AuroraLip/Compression/Formats/PRS_BE.cs b/lib/AuroraLip/Compression/Formats/PRS_BE.cs new file mode 100644 index 00000000..4978de37 --- /dev/null +++ b/lib/AuroraLip/Compression/Formats/PRS_BE.cs @@ -0,0 +1,25 @@ +using AuroraLib.Common; + +namespace AuroraLib.Compression.Formats +{ + /// + /// The PRS compression algorithm is based on LZ77 with run-length encoding emulation and extended matches. + /// + public partial class PRS_BE : ICompression + { + public bool CanRead => true; + + public bool CanWrite => true; + + public static Endian Order => Endian.Big; + + public bool IsMatch(Stream stream, in string extension = "") + => stream.Length > 6 && (stream.ReadByte() & 128) == 128 && stream.At(-2, SeekOrigin.End, S => S.ReadUInt16()) == 0; + + public byte[] Decompress(Stream source) + => PRS.Decompress_ALG(source, Order); + + public void Compress(in byte[] source, Stream destination) + => PRS.Compress_ALG(source, destination, Order); + } +}