ModernUO/Projects/Server/Network/NetworkCompression.cs
Kamron Batman 065f5c5117
fix(network): restore huffman code for symbol 0x19
#2522 rewrote the outgoing huffman table and transposed symbol 0x19's code
from 0x1CE to 0x12E. The resulting code (100101110) is prefixed by symbol
0x0D's code (10010111), so the table was no longer prefix-free: the client's
decoder matched 0x0D after 8 bits, emitted the wrong byte, and desynced the
framing of everything that followed.

Any outgoing packet containing byte 0x19 anywhere in its body was affected.
StatLockInfo (0xBF/0x19) is sent during login, so clients jammed shortly after
entering the world - frozen world state, no disconnect.

Validated all 257 entries against every revision of the table in history: all
34 revisions back to the original import agree, and 0x1CE is the only outlier.

Adds regression tests: known-answer vectors generated from the canonical table
(pinning every entry), and a structural check that the table is a complete
prefix-free code, which is what makes this class of typo detectable at all.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 09:18:00 -07:00

183 lines
7.8 KiB
C#

using System;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Server.Network;
/// <summary>
/// Handles outgoing packet compression for the network.
/// </summary>
public static class NetworkCompression
{
// UO packets may not exceed 64kb in length
public const int BufferSize = 0x10000;
// Optimal compression ratio is 2 / 8; worst compression ratio is 11 / 8
private const int MinimalCodeLength = 2;
// Fixed overhead, in bits, per compression call
private const int TerminalCodeLength = 4;
// If our input exceeds this length, we cannot possibly compress it within the buffer
private const int DefiniteOverflow = (BufferSize * 8 - TerminalCodeLength) / MinimalCodeLength;
// Packed Table: High 16 bits = Length, Low 16 bits = Value
private static readonly uint[] _packedHuffmanTable = new uint[257];
static NetworkCompression()
{
ReadOnlySpan<int> rawTable = [
0x2, 0x000, 0x5, 0x01F, 0x6, 0x022, 0x7, 0x034, 0x7, 0x075, 0x6, 0x028, 0x6, 0x03B, 0x7, 0x032,
0x8, 0x0E0, 0x8, 0x062, 0x7, 0x056, 0x8, 0x079, 0x9, 0x19D, 0x8, 0x097, 0x6, 0x02A, 0x7, 0x057,
0x8, 0x071, 0x8, 0x05B, 0x9, 0x1CC, 0x8, 0x0A7, 0x7, 0x025, 0x7, 0x04F, 0x8, 0x066, 0x8, 0x07D,
0x9, 0x191, 0x9, 0x1CE, 0x7, 0x03F, 0x9, 0x090, 0x8, 0x059, 0x8, 0x07B, 0x8, 0x091, 0x8, 0x0C6,
0x6, 0x02D, 0x9, 0x186, 0x8, 0x06F, 0x9, 0x093, 0xA, 0x1CC, 0x8, 0x05A, 0xA, 0x1AE, 0xA, 0x1C0,
0x9, 0x148, 0x9, 0x14A, 0x9, 0x082, 0xA, 0x19F, 0x9, 0x171, 0x9, 0x120, 0x9, 0x0E7, 0xA, 0x1F3,
0x9, 0x14B, 0x9, 0x100, 0x9, 0x190, 0x6, 0x013, 0x9, 0x161, 0x9, 0x125, 0x9, 0x133, 0x9, 0x195,
0x9, 0x173, 0x9, 0x1CA, 0x9, 0x086, 0x9, 0x1E9, 0x9, 0x0DB, 0x9, 0x1EC, 0x9, 0x08B, 0x9, 0x085,
0x5, 0x00A, 0x8, 0x096, 0x8, 0x09C, 0x9, 0x1C3, 0x9, 0x19C, 0x9, 0x08F, 0x9, 0x18F, 0x9, 0x091,
0x9, 0x087, 0x9, 0x0C6, 0x9, 0x177, 0x9, 0x089, 0x9, 0x0D6, 0x9, 0x08C, 0x9, 0x1EE, 0x9, 0x1EB,
0x9, 0x084, 0x9, 0x164, 0x9, 0x175, 0x9, 0x1CD, 0x8, 0x05E, 0x9, 0x088, 0x9, 0x12B, 0x9, 0x172,
0x9, 0x10A, 0x9, 0x08D, 0x9, 0x13A, 0x9, 0x11C, 0xA, 0x1E1, 0xA, 0x1E0, 0x9, 0x187, 0xA, 0x1DC,
0xA, 0x1DF, 0x7, 0x074, 0x9, 0x19F, 0x8, 0x08D, 0x8, 0x0E4, 0x7, 0x079, 0x9, 0x0EA, 0x9, 0x0E1,
0x8, 0x040, 0x7, 0x041, 0x9, 0x10B, 0x9, 0x0B0, 0x8, 0x06A, 0x8, 0x0C1, 0x7, 0x071, 0x7, 0x078,
0x8, 0x0B1, 0x9, 0x14C, 0x7, 0x043, 0x8, 0x076, 0x7, 0x066, 0x7, 0x04D, 0x9, 0x08A, 0x6, 0x02F,
0x8, 0x0C9, 0x9, 0x0CE, 0x9, 0x149, 0x9, 0x160, 0xA, 0x1BA, 0xA, 0x19E, 0xA, 0x39F, 0x9, 0x0E5,
0x9, 0x194, 0x9, 0x184, 0x9, 0x126, 0x7, 0x030, 0x8, 0x06C, 0x9, 0x121, 0x9, 0x1E8, 0xA, 0x1C1,
0xA, 0x11D, 0xA, 0x163, 0xA, 0x385, 0xA, 0x3DB, 0xA, 0x17D, 0xA, 0x106, 0xA, 0x397, 0xA, 0x24E,
0x7, 0x02E, 0x8, 0x098, 0xA, 0x33C, 0xA, 0x32E, 0xA, 0x1E9, 0x9, 0x0BF, 0xA, 0x3DF, 0xA, 0x1DD,
0xA, 0x32D, 0xA, 0x2ED, 0xA, 0x30B, 0xA, 0x107, 0xA, 0x2E8, 0xA, 0x3DE, 0xA, 0x125, 0xA, 0x1E8,
0x9, 0x0E9, 0xA, 0x1CD, 0xA, 0x1B5, 0x9, 0x165, 0xA, 0x232, 0xA, 0x2E1, 0xB, 0x3AE, 0xB, 0x3C6,
0xB, 0x3E2, 0xA, 0x205, 0xA, 0x29A, 0xA, 0x248, 0xA, 0x2CD, 0xA, 0x23B, 0xB, 0x3C5, 0xA, 0x251,
0xA, 0x2E9, 0xA, 0x252, 0x9, 0x1EA, 0xB, 0x3A0, 0xB, 0x391, 0xA, 0x23C, 0xB, 0x392, 0xB, 0x3D5,
0xA, 0x233, 0xA, 0x2CC, 0xB, 0x390, 0xA, 0x1BB, 0xB, 0x3A1, 0xB, 0x3C4, 0xA, 0x211, 0xA, 0x203,
0x9, 0x12A, 0xA, 0x231, 0xB, 0x3E0, 0xA, 0x29B, 0xB, 0x3D7, 0xA, 0x202, 0xB, 0x3AD, 0xA, 0x213,
0xA, 0x253, 0xA, 0x32C, 0xA, 0x23D, 0xA, 0x23F, 0xA, 0x32F, 0xA, 0x11C, 0xA, 0x384, 0xA, 0x31C,
0xA, 0x17C, 0xA, 0x30A, 0xA, 0x2E0, 0xA, 0x276, 0xA, 0x250, 0xB, 0x3E3, 0xA, 0x396, 0xA, 0x18F,
0xA, 0x204, 0xA, 0x206, 0xA, 0x230, 0xA, 0x265, 0xA, 0x212, 0xA, 0x23E, 0xB, 0x3AC, 0xB, 0x393,
0xB, 0x3E1, 0xA, 0x1DE, 0xB, 0x3D6, 0xA, 0x31D, 0xB, 0x3E5, 0xB, 0x3E4, 0xA, 0x207, 0xB, 0x3C7,
0xA, 0x277, 0xB, 0x3D4, 0x8, 0x0C0, 0xA, 0x162, 0xA, 0x3DA, 0xA, 0x124, 0xA, 0x1B4, 0xA, 0x264,
0xA, 0x33D, 0xA, 0x1D1, 0xA, 0x1AF, 0xA, 0x39E, 0xA, 0x24F, 0xB, 0x373, 0xA, 0x249, 0xB, 0x372,
0x9, 0x167, 0xA, 0x210, 0xA, 0x23A, 0xA, 0x1B8, 0xB, 0x3AF, 0xA, 0x18E, 0xA, 0x2EC, 0x7, 0x062,
0x4, 0x00D
];
for (var i = 0; i < 257; i++)
{
var len = (uint)rawTable[i * 2];
var val = (uint)rawTable[i * 2 + 1];
_packedHuffmanTable[i] = (len << 16) | val;
}
}
public static int Compress(ReadOnlySpan<byte> input, Span<byte> output)
{
if (input.Length > DefiniteOverflow)
{
return 0;
}
ref var inputRef = ref MemoryMarshal.GetReference(input);
ref var outputRef = ref MemoryMarshal.GetReference(output);
ref var tableRef = ref MemoryMarshal.GetReference(_packedHuffmanTable.AsSpan());
nuint i = 0;
ulong bitValue = 0;
var bitCount = 0;
nuint outputIdx = 0;
var safeOutputLength = (nuint)output.Length - 4;
var unrolledLimit = (nuint)input.Length - 1;
// Unrolled 2x hot loop using native integer indices
while (i < unrolledLimit)
{
var b1 = Unsafe.Add(ref inputRef, i);
var entry1 = Unsafe.Add(ref tableRef, b1);
var len1 = (int)(entry1 >> 16);
ulong val1 = entry1 & 0xFFFF;
bitValue = (bitValue << len1) | val1;
bitCount += len1;
var b2 = Unsafe.Add(ref inputRef, i + 1);
var entry2 = Unsafe.Add(ref tableRef, b2);
var len2 = (int)(entry2 >> 16);
ulong val2 = entry2 & 0xFFFF;
bitValue = (bitValue << len2) | val2;
bitCount += len2;
if (bitCount >= 32)
{
if (outputIdx > safeOutputLength)
{
return 0;
}
bitCount -= 32;
var word = (uint)(bitValue >> bitCount);
Unsafe.WriteUnaligned(ref Unsafe.Add(ref outputRef, outputIdx), BinaryPrimitives.ReverseEndianness(word));
outputIdx += 4;
}
i += 2;
}
if (i < (nuint)input.Length)
{
var b = Unsafe.Add(ref inputRef, i);
var entry = Unsafe.Add(ref tableRef, b);
var len = (int)(entry >> 16);
ulong val = entry & 0xFFFF;
bitValue = (bitValue << len) | val;
bitCount += len;
if (bitCount >= 32)
{
if (outputIdx > safeOutputLength)
{
return 0;
}
bitCount -= 32;
var word = (uint)(bitValue >> bitCount);
Unsafe.WriteUnaligned(ref Unsafe.Add(ref outputRef, outputIdx), BinaryPrimitives.ReverseEndianness(word));
outputIdx += 4;
}
}
// Terminal code logic
var termEntry = Unsafe.Add(ref tableRef, 256);
var termLen = (int)(termEntry >> 16);
ulong termVal = termEntry & 0xFFFF;
bitValue = (bitValue << termLen) | termVal;
bitCount += termLen;
var remainder = bitCount & 7;
if (remainder != 0)
{
var padding = 8 - remainder;
bitValue <<= padding;
bitCount += padding;
}
bitValue <<= 64 - bitCount;
while (bitCount > 0)
{
if (outputIdx >= (nuint)output.Length)
{
return 0;
}
Unsafe.Add(ref outputRef, outputIdx++) = (byte)(bitValue >> 56);
bitValue <<= 8;
bitCount -= 8;
}
return (int)outputIdx;
}
}