From 2d5fa21c00028f04fcd3e3c894d008d8575933bf Mon Sep 17 00:00:00 2001 From: Ray Koopa Date: Wed, 16 Jan 2019 06:28:14 +0100 Subject: [PATCH] Clone original WWPA compression as close as possible due to bug found in original implementation. --- .../Packets/AppConnection.cs | 1 + .../WorldPartyAqua/PacketCompression.cs | 196 +++++++---- .../PacketCompressionTests.cs | 65 ++++ .../Syroot.Worms.Mgame.Test.csproj | 12 + src/Syroot.Worms.Mgame/PacketCompression.cs | 332 ++++++++++++++++++ src/Syroot.Worms.Scratchpad/Program.cs | 45 ++- .../Syroot.Worms.Scratchpad.csproj | 1 - src/Syroot.Worms.sln | 7 + 8 files changed, 587 insertions(+), 72 deletions(-) create mode 100644 src/Syroot.Worms.Mgame.Test/PacketCompressionTests.cs create mode 100644 src/Syroot.Worms.Mgame.Test/Syroot.Worms.Mgame.Test.csproj create mode 100644 src/Syroot.Worms.Mgame/PacketCompression.cs diff --git a/src/Syroot.Worms.Mgame.GameServer/Packets/AppConnection.cs b/src/Syroot.Worms.Mgame.GameServer/Packets/AppConnection.cs index f9dd043..aae1d93 100644 --- a/src/Syroot.Worms.Mgame.GameServer/Packets/AppConnection.cs +++ b/src/Syroot.Worms.Mgame.GameServer/Packets/AppConnection.cs @@ -248,6 +248,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets { // Retrieve (decompressed) data size. ReadOnlySpan decompressedData = _packetDataStream.GetSpan(); + decompressedData = File.ReadAllBytes(@"D:\Pictures\data.bin"); ReadOnlySpan compressedData = PacketCompression.Compress(decompressedData); // Send head. diff --git a/src/Syroot.Worms.Mgame.GameServer/Packets/WorldPartyAqua/PacketCompression.cs b/src/Syroot.Worms.Mgame.GameServer/Packets/WorldPartyAqua/PacketCompression.cs index 9474ca0..97fde08 100644 --- a/src/Syroot.Worms.Mgame.GameServer/Packets/WorldPartyAqua/PacketCompression.cs +++ b/src/Syroot.Worms.Mgame.GameServer/Packets/WorldPartyAqua/PacketCompression.cs @@ -6,71 +6,119 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua /// Represents packet data encryption as used in WWPA. Note that most code is a direct translation from the original /// game assembly. /// + /// + /// The original implementation has the following bugs caused by specific, but rare patterns (in 1000 random blobs + /// of sizes between 1 and 1000 bytes, ~0.5% were decompressed badly): + /// - Bad calculation of decompressed bytes. The same wrong bytes are calculated by this implementation. Such data + /// may be discarded by the game as soon as it causes the final length to differ. + /// - Bad negative source buffer indices cause out-of-bounds memory to be written. Combatted in this implementation + /// by ignoring such writes and keeping 0 bytes instead. + /// public static class PacketCompression { // ---- FIELDS ------------------------------------------------------------------------------------------------- + private static int _numBytesTransferred = 0; + + private static int _field_4 = 4; + private static int _field_8 = 0; + + private static int _field_C = 0; + private static int _field_10 = 0; + + private static int[] _bufferDwords = new int[512]; + private static byte[] _buffer = new byte[256]; - private static int _cursor = 0; + private static int _bufferCursor = 0; // ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- + /// + /// Compresses the given data. + /// + /// The data to compress. + /// The compressed data. public static ReadOnlySpan Compress(ReadOnlySpan decompressed) { Compressor compressor = new Compressor(); + int idx; + int bytesToRepeat; + int idxDword; + int i; + int offset; + int v11; + int v12; + int field_8; + int field_10; + byte c; + int shiftValue1; - int idx = 0; + idx = 0; while (idx < decompressed.Length) { - int value1 = 0; - int value2 = -1; - int bytesToRepeat = Math.Max(0, idx - 0xFF); + bytesToRepeat = idx - 0xFF; + if (idx - 0xFF < 0) + bytesToRepeat = 0; + idxDword = 0; + shiftValue1 = -1; if (bytesToRepeat >= idx) { - Write(compressor, decompressed[idx++]); + c = decompressed[idx++]; + Write(compressor, c); } else { do { - int i; - for (i = idx; i < decompressed.Length; i++) + for (i = idx; i < decompressed.Length; ++i) { - if (i - idx >= 0x111 - || decompressed[bytesToRepeat] != decompressed[i]) + if (i - idx >= 0x111) break; - bytesToRepeat++; + if (decompressed[bytesToRepeat] != decompressed[i]) + break; + ++bytesToRepeat; } - int offset = i - idx; - if (offset >= 3 && offset > value1) + offset = idx - i; + v11 = i - idx; + v12 = offset + bytesToRepeat; + if (v11 >= 3 && v11 > idxDword) { - value1 = offset; - value2 = idx - 12; - if (offset == 0x111) + idxDword = v11; + shiftValue1 = idx - 12; + if (v11 == 0x111) break; } - bytesToRepeat += idx - i + 1; - } while (bytesToRepeat < idx); - if (value1 != 0) + bytesToRepeat = v12 + 1; + } + while (bytesToRepeat < idx); + if (idxDword != 0) { TransferBuffer(compressor); compressor.Compress(true); - if (value1 >= 18) + if (idxDword >= 18) { compressor.Shift(0, 4); - compressor.Shift(value2, 8); - compressor.Shift(value1 - 18, 8); + compressor.Shift(shiftValue1, 8); + compressor.Shift(idxDword - 18, 8); + field_10 = _field_10; + ++_field_C; + _field_10 = idxDword - 3 + field_10; } else { - compressor.Shift(value1 - 2, 4); - compressor.Shift(value2 - 1, 8); + compressor.Shift(idxDword - 2, 4); + compressor.Shift(shiftValue1 - 1, 8); + field_8 = _field_8; + ++_field_4; + _field_8 = idxDword - 2 + field_8; } - idx += value1; + idx += idxDword; + ++_bufferDwords[idxDword]; } else { - Write(compressor, decompressed[idx++]); + c = decompressed[idx++]; + Write(compressor, c); } } } @@ -79,17 +127,22 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua compressor.Shift(0, 4); compressor.Shift(0, 8); - return compressor.Buffer.AsSpan(0, compressor.Cursor); + return compressor.Compressed.AsSpan(0, compressor.Cursor); + } public static ReadOnlySpan Decompress(ReadOnlySpan compressed) { Decompressor decompressor = new Decompressor(compressed); byte[] decompressed = new byte[2048]; - - int bytesToCopy; + int idx; int bytesToCopyRemain; - int idx = 0; + int idxCopySrc; + int v12; + int v13; + int bytesToCopy; + + idx = 0; while (true) { while (true) @@ -99,42 +152,44 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua bytesToCopy = decompressor.GetBytesToCopy(8); if (bytesToCopy != -1) { - bytesToCopy++; + ++bytesToCopy; do { decompressed[idx++] = decompressor.Read(); - bytesToCopy--; + --bytesToCopy; } while (bytesToCopy != 0); } } - if (decompressor.GetBytesToCopy(4) + 2 <= 2) + bytesToCopy = decompressor.GetBytesToCopy(4) + 2; + if (bytesToCopy <= 2) break; bytesToCopy = decompressor.GetBytesToCopy(8) + 1; bytesToCopyRemain = bytesToCopy - 1; if (bytesToCopy != 0) { - bytesToCopy++; + ++bytesToCopyRemain; do { - int idxCopySrc = idx++ - bytesToCopy; - bytesToCopyRemain--; - decompressed[idx - 1] = decompressed[idxCopySrc]; + idxCopySrc = idx++ - bytesToCopy; + --bytesToCopyRemain; + if (idxCopySrc >= 0) // Bugfix: Original implementation may calculate negative indices. + decompressed[idx - 1] = decompressed[idxCopySrc]; } while (bytesToCopyRemain != 0); } } bytesToCopy = decompressor.GetBytesToCopy(8); if (bytesToCopy == 0) break; - int v12 = decompressor.GetBytesToCopy(8); - int v13 = v12 + 18; + v12 = decompressor.GetBytesToCopy(8); + v13 = v12 + 18; bytesToCopyRemain = v12 + 17; if (v13 != 0) { - bytesToCopyRemain++; + ++bytesToCopyRemain; do { - int idxCopySrc = idx++ - bytesToCopy; - bytesToCopyRemain--; + idxCopySrc = idx++ - bytesToCopy; + --bytesToCopyRemain; decompressed[idx - 1] = decompressed[idxCopySrc]; } while (bytesToCopyRemain != 0); } @@ -146,20 +201,25 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua private static void TransferBuffer(Compressor compressor) { - if (_cursor != 0) + int bufferCursor; + int i; + + bufferCursor = _bufferCursor; + if (bufferCursor != 0) { + _numBytesTransferred += bufferCursor; compressor.Compress(false); - compressor.Shift(_cursor - 1, 8); - for (int i = 0; i < _cursor; i++) - compressor.Buffer[compressor.Cursor++] = _buffer[i]; - _cursor = 0; + compressor.Shift(_bufferCursor - 1, 8); + for (i = 0; i < _bufferCursor; ++i) + compressor.Write(_buffer[i]); + _bufferCursor = 0; } } private static void Write(Compressor compressor, byte c) { - _buffer[_cursor++] = c; - if (_cursor >= _buffer.Length) + _buffer[_bufferCursor++] = c; + if (_bufferCursor >= _buffer.Length) TransferBuffer(compressor); } @@ -167,28 +227,41 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua private class Compressor { - internal byte[] Buffer = new byte[1024]; + internal byte[] Compressed = new byte[1024]; internal int Cursor = 1; private int _cursorCompress = 0; private int _shift = 0; - internal void Compress(bool doShift) + internal void Compress(bool bShift) { - if (doShift) - Buffer[_cursorCompress] |= (byte)(0x80 >> _shift); - if (++_shift == 8) + int shift; + int cursor; + int cursorCompress; + + if (bShift) { + Compressed[_cursorCompress] |= (byte)(0x80 >> _shift); + } + shift = _shift + 1; + _shift = shift; + if (shift == 8) + { + cursor = Cursor; _cursorCompress = Cursor++; - Buffer[_cursorCompress] = 0; + cursorCompress = _cursorCompress; + Cursor = cursor + 1; + Compressed[cursorCompress] = 0; _shift = 0; } } internal void Shift(int value, int shiftPlus1) { - int prevShift = 0; - int shift = shiftPlus1 - 1; + int shift; + int prevShift; + + shift = shiftPlus1 - 1; if (shiftPlus1 != 0) { do @@ -198,11 +271,16 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua } while (prevShift != 0); } } + + internal void Write(byte c) + { + Compressed[Cursor++] = c; + } } private class Decompressor { - private byte[] _compressed; + private readonly byte[] _compressed; private int _compressedCursor = 0; private byte _field_C; private byte _field_D; @@ -212,7 +290,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua _compressed = compressed.ToArray(); byte c = Read(); _field_C = (byte)(2 * c | 1); - _field_D = (byte)(c >> 7); + _field_D = (byte)((uint)c >> 7); } internal byte Read() diff --git a/src/Syroot.Worms.Mgame.Test/PacketCompressionTests.cs b/src/Syroot.Worms.Mgame.Test/PacketCompressionTests.cs new file mode 100644 index 0000000..8ed0035 --- /dev/null +++ b/src/Syroot.Worms.Mgame.Test/PacketCompressionTests.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Syroot.Worms.Mgame.Test +{ + [TestClass] + public class PacketCompressionTests + { + // ---- FIELDS ------------------------------------------------------------------------------------------------- + + // Known to cause 3 wrong bytes: + private static readonly byte[] _origBadBytes = new byte[] { 0x68, 0x56, 0xF9, 0x1D, 0x39, 0x5F, 0xD0, 0x57, 0x13, 0x9B, 0xF3, 0x13, 0xF5, 0x97, 0x41, 0xD0, 0x57, 0x13, 0x1E, 0x6C, 0x4A }; + private static readonly byte[] _rcmpBadBytes = new byte[] { 0x68, 0x56, 0xF9, 0x1D, 0x39, 0x5F, 0xD0, 0x57, 0x13, 0x9B, 0xF3, 0x13, 0xF5, 0x97, 0x41, 0xF5, 0x97, 0x41, 0x1E, 0x6C, 0x4A }; + + // Known to cause negative index bug writing undefined bytes: + private static readonly byte[] _origBadIndex = new byte[] { 0xFC, 0xFC, 0xC2, 0x92, 0x8F, 0x66, 0x33, 0x44, 0xEF, 0x40, 0xE9, 0xAB, 0x44, 0xEF, 0x40, 0x36 }; + private static readonly byte[] _rcmpBadIndex = new byte[] { 0xFC, 0xFC, 0xC2, 0x92, 0x8F, 0x66, 0x33, 0x44, 0xEF, 0x40, 0xE9, 0xAB, 0x00, 0x00, 0x00, 0x36 }; + + // ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- + + [TestMethod] + public void TestBadBytes() + { + Assert.IsTrue(Recompress(_origBadBytes).SequenceEqual(_rcmpBadBytes)); + } + + [TestMethod] + public void TestBadIndex() + { + var recompressed = Recompress(_origBadIndex); + Assert.IsTrue(recompressed.SequenceEqual(_rcmpBadIndex)); + } + + [TestMethod] + public void TestRandom() + { + const int runs = 1000; + int fails = 0; + Random random = new Random(); + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + { + for (int i = 0; i < runs; i++) + { + byte[] data = new byte[random.Next(1, 1000)]; + rng.GetBytes(data); + ReadOnlySpan recompressed = Recompress(data); + if (!data.AsSpan().SequenceEqual(recompressed)) + fails++; + } + } + // If less than 1% fails, the compression is seen as working as in the original implementation. + Assert.IsTrue(fails / (float)runs < 0.01); + } + + // ---- METHODS (PRIVATE) -------------------------------------------------------------------------------------- + + private static ReadOnlySpan Recompress(ReadOnlySpan data) + { + ReadOnlySpan compressed = PacketCompression.Compress(data); + return PacketCompression.Decompress(compressed); + } + } +} diff --git a/src/Syroot.Worms.Mgame.Test/Syroot.Worms.Mgame.Test.csproj b/src/Syroot.Worms.Mgame.Test/Syroot.Worms.Mgame.Test.csproj new file mode 100644 index 0000000..70b6f3a --- /dev/null +++ b/src/Syroot.Worms.Mgame.Test/Syroot.Worms.Mgame.Test.csproj @@ -0,0 +1,12 @@ + + + netcoreapp2.1 + false + + + + + + + + diff --git a/src/Syroot.Worms.Mgame/PacketCompression.cs b/src/Syroot.Worms.Mgame/PacketCompression.cs new file mode 100644 index 0000000..9b5ff44 --- /dev/null +++ b/src/Syroot.Worms.Mgame/PacketCompression.cs @@ -0,0 +1,332 @@ +using System; + +namespace Syroot.Worms.Mgame +{ + /// + /// Represents packet data encryption as used in WWPA. Note that most code is a direct translation from the original + /// game assembly. + /// + /// + /// The original implementation has the following bugs caused by specific, but rare patterns (in 1000 random blobs + /// of sizes between 1 and 1000 bytes, ~0.5% were decompressed badly): + /// - Bad calculation of decompressed bytes. The same wrong bytes are calculated by this implementation. + /// - Bad negative source buffer indices cause out-of-bounds memory to be written. Combatted in this implementation + /// by ignoring such writes and keeping 0 bytes instead. + /// The game discards decompression results of a length longer than specified in the packet header, but does not + /// check the data itself if the length is still fine. + /// + public static class PacketCompression + { + // ---- FIELDS ------------------------------------------------------------------------------------------------- + + private static int _numBytesTransferred = 0; + + private static int _field_4 = 4; + private static int _field_8 = 0; + + private static int _field_C = 0; + private static int _field_10 = 0; + + private static int[] _bufferDwords = new int[512]; + + private static byte[] _buffer = new byte[256]; + private static int _bufferCursor = 0; + + // ---- METHODS (INTERNAL) ------------------------------------------------------------------------------------- + + /// + /// Compresses the given data. + /// + /// The data to compress. + /// The compressed data. + public static ReadOnlySpan Compress(ReadOnlySpan decompressed) + { + Compressor compressor = new Compressor(); + int idx; + int bytesToRepeat; + int idxDword; + int i; + int offset; + int v11; + int v12; + int field_8; + int field_10; + byte c; + int shiftValue1; + + idx = 0; + while (idx < decompressed.Length) + { + bytesToRepeat = idx - 0xFF; + if (idx - 0xFF < 0) + bytesToRepeat = 0; + idxDword = 0; + shiftValue1 = -1; + if (bytesToRepeat >= idx) + { + c = decompressed[idx++]; + Write(compressor, c); + } + else + { + do + { + for (i = idx; i < decompressed.Length; ++i) + { + if (i - idx >= 0x111) + break; + if (decompressed[bytesToRepeat] != decompressed[i]) + break; + ++bytesToRepeat; + } + offset = idx - i; + v11 = i - idx; + v12 = offset + bytesToRepeat; + if (v11 >= 3 && v11 > idxDword) + { + idxDword = v11; + shiftValue1 = idx - 12; + if (v11 == 0x111) + break; + } + bytesToRepeat = v12 + 1; + } + while (bytesToRepeat < idx); + if (idxDword != 0) + { + TransferBuffer(compressor); + compressor.Compress(true); + if (idxDword >= 18) + { + compressor.Shift(0, 4); + compressor.Shift(shiftValue1, 8); + compressor.Shift(idxDword - 18, 8); + field_10 = _field_10; + ++_field_C; + _field_10 = idxDword - 3 + field_10; + } + else + { + compressor.Shift(idxDword - 2, 4); + compressor.Shift(shiftValue1 - 1, 8); + field_8 = _field_8; + ++_field_4; + _field_8 = idxDword - 2 + field_8; + } + idx += idxDword; + ++_bufferDwords[idxDword]; + } + else + { + c = decompressed[idx++]; + Write(compressor, c); + } + } + } + TransferBuffer(compressor); + compressor.Compress(true); + compressor.Shift(0, 4); + compressor.Shift(0, 8); + + return compressor.Compressed.AsSpan(0, compressor.Cursor); + + } + + public static ReadOnlySpan Decompress(ReadOnlySpan compressed) + { + Decompressor decompressor = new Decompressor(compressed); + byte[] decompressed = new byte[2048]; + int idx; + int v5; + int v6; + int idxCopySrc; + int v11; + int v12; + int v13; + int bytesToCopy; + + idx = 0; + while (true) + { + while (true) + { + while (decompressor.sub_4C0BA0() == 0) + { + bytesToCopy = decompressor.GetBytesToCopy(8); + if (bytesToCopy != -1) + { + ++bytesToCopy; + do + { + decompressed[idx++] = decompressor.Read(); + --bytesToCopy; + } while (bytesToCopy != 0); + } + } + v5 = decompressor.GetBytesToCopy(4) + 2; + if (v5 <= 2) + break; + v6 = decompressor.GetBytesToCopy(8) + 1; + bytesToCopy = v5 - 1; + if (v5 != 0) + { + ++bytesToCopy; + do + { + idxCopySrc = idx++ - v6; + --bytesToCopy; + if (idxCopySrc >= 0) // Bugfix: Original implementation may calculate negative indices. + decompressed[idx - 1] = decompressed[idxCopySrc]; + } while (bytesToCopy != 0); + } + } + v11 = decompressor.GetBytesToCopy(8); + if (v11 == 0) + break; + v12 = decompressor.GetBytesToCopy(8); + v13 = v12 + 18; + bytesToCopy = v12 + 17; + if (v13 != 0) + { + ++bytesToCopy; + do + { + idxCopySrc = idx++ - v11; + --bytesToCopy; + decompressed[idx - 1] = decompressed[idxCopySrc]; + } while (bytesToCopy != 0); + } + } + return decompressed.AsSpan(0, idx); + } + + // ---- METHODS (PRIVATE) -------------------------------------------------------------------------------------- + + private static void TransferBuffer(Compressor compressor) + { + int bufferCursor; + int i; + + bufferCursor = _bufferCursor; + if (bufferCursor != 0) + { + _numBytesTransferred += bufferCursor; + compressor.Compress(false); + compressor.Shift(_bufferCursor - 1, 8); + for (i = 0; i < _bufferCursor; ++i) + compressor.Write(_buffer[i]); + _bufferCursor = 0; + } + } + + private static void Write(Compressor compressor, byte c) + { + _buffer[_bufferCursor++] = c; + if (_bufferCursor >= _buffer.Length) + TransferBuffer(compressor); + } + + // ---- CLASSES, STRUCTS & ENUMS ------------------------------------------------------------------------------- + + private class Compressor + { + internal byte[] Compressed = new byte[1024]; + internal int Cursor = 1; + + private int _cursorCompress = 0; + private int _shift = 0; + + internal void Compress(bool bShift) + { + int shift; + int cursor; + int cursorCompress; + + if (bShift) + { + Compressed[_cursorCompress] |= (byte)(0x80 >> _shift); + } + shift = _shift + 1; + _shift = shift; + if (shift == 8) + { + cursor = Cursor; + _cursorCompress = Cursor++; + cursorCompress = _cursorCompress; + Cursor = cursor + 1; + Compressed[cursorCompress] = 0; + _shift = 0; + } + } + + internal void Shift(int value, int shiftPlus1) + { + int shift; + int prevShift; + + shift = shiftPlus1 - 1; + if (shiftPlus1 != 0) + { + do + { + Compress(((value >> shift) & 1) != 0); + prevShift = shift--; + } while (prevShift != 0); + } + } + + internal void Write(byte c) + { + Compressed[Cursor++] = c; + } + } + + private class Decompressor + { + private readonly byte[] _compressed; + private int _compressedCursor = 0; + private byte _field_C; + private byte _field_D; + + internal Decompressor(ReadOnlySpan compressed) + { + _compressed = compressed.ToArray(); + byte c = Read(); + _field_C = (byte)(2 * c | 1); + _field_D = (byte)((uint)c >> 7); + } + + internal byte Read() + { + return _compressed[_compressedCursor++]; + } + + internal int GetBytesToCopy(int numCalls) + { + if (numCalls == 0) + return 0; + int bytesToCopy = 0; + do + { + numCalls--; + bytesToCopy = sub_4C0BA0() + 2 * bytesToCopy; + } while (numCalls != 0); + return bytesToCopy; + } + + internal int sub_4C0BA0() + { + byte field_D = _field_D; + _field_D = (byte)(_field_C >> 7); + _field_C *= 2; + if (_field_C == 0) + { + byte c = Read(); + _field_C = (byte)(2 * c | 1); + _field_D = (byte)(c >> 7); + } + return field_D; + } + } + } +} diff --git a/src/Syroot.Worms.Scratchpad/Program.cs b/src/Syroot.Worms.Scratchpad/Program.cs index 30ba6c2..bf72a99 100644 --- a/src/Syroot.Worms.Scratchpad/Program.cs +++ b/src/Syroot.Worms.Scratchpad/Program.cs @@ -1,10 +1,7 @@ using System; -using System.Diagnostics; using System.Drawing.Imaging; using System.IO; -using Syroot.BinaryData; using Syroot.Worms.Mgame; -using Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua; namespace Syroot.Worms.Scratchpad { @@ -14,19 +11,43 @@ namespace Syroot.Worms.Scratchpad private static void Main(string[] args) { - DecompressWwpaPacketData(); + TestWwpaPacketCompressionBug(); + Console.ReadLine(); } - private static void DecompressWwpaPacketData() + private static void TestWwpaPacketCompressionBug() { - byte[] decompressed = new byte[sizeof(int)]; - ByteConverter.System.GetBytes(0x8001, decompressed); - ReadOnlySpan compressed = PacketCompression.Compress(decompressed); - Debug.Assert(compressed.SequenceEqual(new byte[] { 0x01, 0xC0, 0x01, 0x80, 0x00, 0x00, 0x00 }), - "Compression failed"); + ReadOnlySpan recompress(ReadOnlySpan data) + { + ReadOnlySpan compressed = PacketCompression.Compress(data); + return PacketCompression.Decompress(compressed); + } - ReadOnlySpan decompressedNew = PacketCompression.Decompress(compressed); - Debug.Assert(decompressedNew.SequenceEqual(decompressed), "Decompression failed"); + bool test(byte[] data) + { + ReadOnlySpan recompressed = recompress(data); + if (!recompressed.SequenceEqual(data)) + { + Console.WriteLine("Decompression failed."); + if (recompressed.Length < data.Length) + { + Console.WriteLine($" New length smaller by {data.Length - recompressed.Length} bytes."); + } + else + { + for (int i = 0; i < data.Length; i++) + { + if (data[i] != recompressed[i]) + Console.WriteLine($" Byte 0x{i:X4} differs: 0x{data[i]:X2} != 0x{recompressed[i]:X2}"); + } + } + return false; + } + return true; + } + + // Known to cause negative index bug writing undefined bytes: ??, ??, ??, ?? + test(new byte[] { 0xFC, 0xFC, 0xC2, 0x92, 0x8F, 0x66, 0x33, 0x44, 0xEF, 0x40, 0xE9, 0xAB, 0x44, 0xEF, 0x40, 0x36 }); } private static void ConvertIgdImages() diff --git a/src/Syroot.Worms.Scratchpad/Syroot.Worms.Scratchpad.csproj b/src/Syroot.Worms.Scratchpad/Syroot.Worms.Scratchpad.csproj index 40d3374..d501444 100644 --- a/src/Syroot.Worms.Scratchpad/Syroot.Worms.Scratchpad.csproj +++ b/src/Syroot.Worms.Scratchpad/Syroot.Worms.Scratchpad.csproj @@ -9,7 +9,6 @@ - diff --git a/src/Syroot.Worms.sln b/src/Syroot.Worms.sln index 5b74c36..065c947 100644 --- a/src/Syroot.Worms.sln +++ b/src/Syroot.Worms.sln @@ -37,6 +37,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Syroot.Worms.Test", "Syroot EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Syroot.Worms.Mgame.GameServer", "Syroot.Worms.Mgame.GameServer\Syroot.Worms.Mgame.GameServer.csproj", "{392E4CA2-61D9-4BE1-B065-8801A9F102B8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Syroot.Worms.Mgame.Test", "Syroot.Worms.Mgame.Test\Syroot.Worms.Mgame.Test.csproj", "{212F8090-9775-4098-BD44-9ABC01FBE553}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -99,6 +101,10 @@ Global {392E4CA2-61D9-4BE1-B065-8801A9F102B8}.Debug|Any CPU.Build.0 = Debug|Any CPU {392E4CA2-61D9-4BE1-B065-8801A9F102B8}.Release|Any CPU.ActiveCfg = Release|Any CPU {392E4CA2-61D9-4BE1-B065-8801A9F102B8}.Release|Any CPU.Build.0 = Release|Any CPU + {212F8090-9775-4098-BD44-9ABC01FBE553}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {212F8090-9775-4098-BD44-9ABC01FBE553}.Debug|Any CPU.Build.0 = Debug|Any CPU + {212F8090-9775-4098-BD44-9ABC01FBE553}.Release|Any CPU.ActiveCfg = Release|Any CPU + {212F8090-9775-4098-BD44-9ABC01FBE553}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -117,6 +123,7 @@ Global {EF308D4E-26A0-471C-B764-9C4EB713BEAE} = {99E56312-A064-4AD3-8443-0B56A5F76E6B} {351B93B0-301F-42E1-82A0-7FA217154F5D} = {99E56312-A064-4AD3-8443-0B56A5F76E6B} {392E4CA2-61D9-4BE1-B065-8801A9F102B8} = {0B9B0B74-3EB1-46A4-BCCC-F2D6AE59A9EE} + {212F8090-9775-4098-BD44-9ABC01FBE553} = {99E56312-A064-4AD3-8443-0B56A5F76E6B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {1CD4EDE2-A5FB-4A58-A850-3506AB7E7B69}