diff --git a/Projects/Server.Tests/Tests/Network/NetworkCompressionTests.cs b/Projects/Server.Tests/Tests/Network/NetworkCompressionTests.cs index a133676f7..826d44deb 100644 --- a/Projects/Server.Tests/Tests/Network/NetworkCompressionTests.cs +++ b/Projects/Server.Tests/Tests/Network/NetworkCompressionTests.cs @@ -1,22 +1,21 @@ using System; -using System.Collections.Generic; -using System.Reflection; using Server.Network; using Xunit; namespace Server.Tests.Network; /// -/// Guards the outgoing huffman table in . +/// Known-answer test for outgoing huffman compression. /// The table is a contract with the client's hard-coded decompression tree, so a single wrong -/// entry silently corrupts every packet containing that byte and desyncs the client's framing. -/// The expected bytes below were generated from the canonical table (as it existed before #2522), -/// not from the implementation, so they fail if the table drifts. +/// entry -- or a subtle bit-packing error in a future rewrite of -- +/// silently corrupts every affected packet and desyncs the client's framing with no error. +/// A round-trip test cannot catch that, since encoder and decoder would share the same mistake. +/// The expected bytes below were generated from the canonical table, not from the implementation. /// public class NetworkCompressionTests { - // Every symbol 0x00-0xFF exactly once. Any single wrong table entry changes these bytes, - // so this vector pins all 256 entries plus the terminal code. + // Every symbol 0x00-0xFF exactly once. Any single wrong table entry changes these bytes, so this + // pins all 256 entries plus the terminal code, and exercises the encoder end to end. private static ReadOnlySpan AllSymbolsCompressed => [ 0x3F, 0x13, 0x4E, 0xB4, 0x76, 0xCB, 0x81, 0x8A, 0xB3, 0xCE, 0x76, 0x5E, @@ -58,71 +57,12 @@ public class NetworkCompressionTests Span output = stackalloc byte[512]; var length = NetworkCompression.Compress(input, output); - AssertEqual(AllSymbolsCompressed, output[..length]); - } + Assert.Equal(AllSymbolsCompressed.Length, length); - [Fact] - public void Compress_EncodesStatLockInfo() - { - // 0xBF/0x19 StatLockInfo, sent during login. Contains 0x19, the symbol broken by #2522. - ReadOnlySpan input = [0xBF, 0x00, 0x0C, 0x00, 0x19, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]; - ReadOnlySpan expected = [0x80, 0xCC, 0xE9, 0xCE, 0x88, 0x0F, 0x86, 0x80]; - - Span output = stackalloc byte[64]; - var length = NetworkCompression.Compress(input, output); - - AssertEqual(expected, output[..length]); - } - - [Fact] - public void HuffmanTable_IsCompletePrefixFreeCode() - { - var table = (uint[])typeof(NetworkCompression) - .GetField("_packedHuffmanTable", BindingFlags.NonPublic | BindingFlags.Static)! - .GetValue(null)!; - - Assert.Equal(257, table.Length); - - // A code is only decodable if no code is a prefix of another. #2522 gave symbol 0x19 the - // code 100101110, which begins with symbol 0x0D's code 10010111 -- so the client's decoder - // matched 0x0D, emitted the wrong byte, and desynced from there on. - var codes = new Dictionary<(int Length, uint Value), int>(); - var kraft = 0.0; - - for (var symbol = 0; symbol < table.Length; symbol++) - { - var length = (int)(table[symbol] >> 16); - var value = table[symbol] & 0xFFFF; - - Assert.InRange(length, 2, 11); - Assert.True(value < 1u << length, $"Symbol 0x{symbol:X2} value 0x{value:X3} exceeds {length} bits"); - - codes[(length, value)] = symbol; - kraft += Math.Pow(2, -length); - } - - foreach (var ((length, value), symbol) in codes) - { - for (var prefixLength = 2; prefixLength < length; prefixLength++) - { - var prefix = value >> (length - prefixLength); - Assert.False( - codes.TryGetValue((prefixLength, prefix), out var other), - $"Symbol 0x{symbol:X2} code is prefixed by symbol 0x{other:X2}" - ); - } - } - - // Kraft-McMillan equality: the tree is full, every leaf reachable and none wasted. - Assert.Equal(1.0, kraft, 10); - } - - private static void AssertEqual(ReadOnlySpan expected, ReadOnlySpan actual) - { - Assert.Equal(expected.Length, actual.Length); + var expected = AllSymbolsCompressed; for (var i = 0; i < expected.Length; i++) { - Assert.True(expected[i] == actual[i], $"Byte {i}: expected 0x{expected[i]:X2}, got 0x{actual[i]:X2}"); + Assert.True(expected[i] == output[i], $"Byte {i}: expected 0x{expected[i]:X2}, got 0x{output[i]:X2}"); } } }