using System.Buffers.Binary; using AcDream.Core.Net.Cryptography; using AcDream.Core.Net.Packets; namespace AcDream.Core.Net.Tests.Packets; public class BorrowedPacketCodecTests { [Fact] public void TryDecodeBorrowed_AllOptionalFieldsAndFragments_MatchesOwnedDecoder() { const PacketHeaderFlags flags = PacketHeaderFlags.ServerSwitch | PacketHeaderFlags.RequestRetransmit | PacketHeaderFlags.RejectRetransmit | PacketHeaderFlags.AckSequence | PacketHeaderFlags.WorldLoginRequest | PacketHeaderFlags.ConnectRequest | PacketHeaderFlags.ConnectResponse | PacketHeaderFlags.CICMDCommand | PacketHeaderFlags.TimeSync | PacketHeaderFlags.EchoRequest | PacketHeaderFlags.Flow | PacketHeaderFlags.BlobFragments; var optional = new byte[8 + 12 + 8 + 4 + 8 + 32 + 8 + 8 + 8 + 4 + 6]; int position = 0; Fill(optional, ref position, 8, 0x11); WriteUInt32(optional, ref position, 2); WriteUInt32(optional, ref position, 0x10111213); WriteUInt32(optional, ref position, 0x20212223); WriteUInt32(optional, ref position, 1); WriteUInt32(optional, ref position, 0x30313233); WriteUInt32(optional, ref position, 0x40414243); Fill(optional, ref position, 8, 0x44); double serverTime = 12345.6789; ulong cookie = 0xFEEDFACECAFEBABE; uint clientId = 0x50515253; uint serverSeed = 0x60616263; uint clientSeed = 0x70717273; WriteDouble(optional, ref position, serverTime); WriteUInt64(optional, ref position, cookie); WriteUInt32(optional, ref position, clientId); WriteUInt32(optional, ref position, serverSeed); WriteUInt32(optional, ref position, clientSeed); WriteUInt32(optional, ref position, 0); Fill(optional, ref position, 8, 0x81); Fill(optional, ref position, 8, 0x91); double timeSync = 9876.54321; float echoTime = 42.25f; WriteDouble(optional, ref position, timeSync); WriteSingle(optional, ref position, echoTime); WriteUInt32(optional, ref position, 0xA0A1A2A3); WriteUInt16(optional, ref position, 0xB0B1); Assert.Equal(optional.Length, position); byte[] first = BuildFragment( sequence: 99, count: 2, index: 0, queue: 7, payload: [0xC1, 0xC2]); byte[] second = BuildFragment( sequence: 99, count: 2, index: 1, queue: 7, payload: [0xD1, 0xD2, 0xD3]); byte[] body = [.. optional, .. first, .. second]; byte[] datagram = Encode(flags, body); PacketCodec.PacketDecodeResult owned = PacketCodec.TryDecode(datagram, inboundIsaac: null); BorrowedPacketDecodeResult borrowed = PacketCodec.TryDecodeBorrowed(datagram, inboundIsaac: null); AssertEquivalent(owned, borrowed); Assert.Equal(serverTime, borrowed.Packet.Optional.ConnectRequestServerTime); Assert.Equal(cookie, borrowed.Packet.Optional.ConnectRequestCookie); Assert.Equal(clientId, borrowed.Packet.Optional.ConnectRequestClientId); Assert.Equal(serverSeed, borrowed.Packet.Optional.ConnectRequestServerSeed); Assert.Equal(clientSeed, borrowed.Packet.Optional.ConnectRequestClientSeed); Assert.Equal(timeSync, borrowed.Packet.Optional.TimeSync); Assert.Equal(echoTime, borrowed.Packet.Optional.EchoRequestClientTime); Assert.Equal(0xA0A1A2A3u, borrowed.Packet.Optional.FlowBytes); Assert.Equal(0xB0B1, borrowed.Packet.Optional.FlowInterval); Assert.Equal( new uint[] { 0x10111213, 0x20212223 }, ReadRetransmits(borrowed.Packet.Optional)); } [Fact] public void TryDecodeBorrowed_LoginPayload_MatchesOwnedDecoder() { byte[] payload = LoginRequest.Build("borrowed", "packet", 47); byte[] datagram = Encode(PacketHeaderFlags.LoginRequest, payload); PacketCodec.PacketDecodeResult owned = PacketCodec.TryDecode(datagram, inboundIsaac: null); BorrowedPacketDecodeResult borrowed = PacketCodec.TryDecodeBorrowed(datagram, inboundIsaac: null); AssertEquivalent(owned, borrowed); Assert.Equal(payload, borrowed.Packet.Optional.RawBytes.ToArray()); Assert.Equal(payload, borrowed.Packet.Body.ToArray()); Assert.Equal(0, borrowed.Packet.FragmentCount); } [Fact] public void TryDecodeBorrowed_EncryptedChecksum_MatchesOwnedDecoder() { byte[] body = new byte[4]; BinaryPrimitives.WriteUInt32LittleEndian(body, 0x12345678); byte[] seed = [0x44, 0x33, 0x22, 0x11]; byte[] datagram = Encode( PacketHeaderFlags.AckSequence | PacketHeaderFlags.EncryptedChecksum, body, new IsaacRandom(seed)); PacketCodec.PacketDecodeResult owned = PacketCodec.TryDecode( datagram, new IsaacRandom(seed)); BorrowedPacketDecodeResult borrowed = PacketCodec.TryDecodeBorrowed( datagram, new IsaacRandom(seed)); AssertEquivalent(owned, borrowed); } [Fact] public void TryDecodeBorrowed_MalformedPackets_MatchOwnedDecoderErrors() { byte[] shortHeader = new byte[PacketHeader.Size - 1]; AssertSameError(shortHeader); var oversized = new byte[PacketHeader.Size + 2]; new PacketHeader { DataSize = 3 }.Pack(oversized); AssertSameError(oversized); byte[] shortOptional = EncodeUnchecked( PacketHeaderFlags.TimeSync, [0x01, 0x02, 0x03, 0x04]); AssertSameError(shortOptional); byte[] zeroCount = BuildFragment( sequence: 1, count: 0, index: 0, queue: 0, payload: [0x01]); AssertSameError(EncodeUnchecked( PacketHeaderFlags.BlobFragments, zeroCount)); byte[] invalidIndex = BuildFragment( sequence: 1, count: 1, index: 1, queue: 0, payload: [0x01]); AssertSameError(EncodeUnchecked( PacketHeaderFlags.BlobFragments, invalidIndex)); byte[] wrongChecksum = Encode( PacketHeaderFlags.AckSequence, [1, 2, 3, 4]); wrongChecksum[8] ^= 0x80; AssertSameError(wrongChecksum); } [Fact] public void TryDecodeBorrowed_WarmSingleFragmentPath_AllocatesNothing() { byte[] fragment = BuildFragment( sequence: 77, count: 1, index: 0, queue: 5, payload: [0xAA, 0xBB, 0xCC, 0xDD]); byte[] datagram = Encode( PacketHeaderFlags.BlobFragments, fragment); int checksum = DecodeAndRead(datagram); long before = GC.GetAllocatedBytesForCurrentThread(); for (int iteration = 0; iteration < 1_000; iteration++) checksum += DecodeAndRead(datagram); long allocated = GC.GetAllocatedBytesForCurrentThread() - before; Assert.NotEqual(0, checksum); Assert.Equal(0, allocated); } private static int DecodeAndRead(ReadOnlyMemory datagram) { BorrowedPacketDecodeResult decoded = PacketCodec.TryDecodeBorrowed( datagram, inboundIsaac: null); int checksum = decoded.Packet.FragmentCount; foreach (BorrowedMessageFragment fragment in decoded.Packet.Fragments) { checksum += fragment.Header.TotalSize; checksum += fragment.Payload.Span[0]; } return checksum; } private static void AssertEquivalent( PacketCodec.PacketDecodeResult owned, BorrowedPacketDecodeResult borrowed) { Assert.Equal(owned.Error, borrowed.Error); if (!owned.IsOk) return; Packet packet = Assert.IsType(owned.Packet); Assert.Equal(packet.Header, borrowed.Packet.Header); Assert.Equal(packet.BodyBytes, borrowed.Packet.Body.ToArray()); Assert.Equal( packet.Optional.RawBytes, borrowed.Packet.Optional.RawBytes.ToArray()); Assert.Equal( packet.Optional.AckSequence, borrowed.Packet.Optional.AckSequence); Assert.Equal( packet.Optional.TimeSync, borrowed.Packet.Optional.TimeSync); Assert.Equal( packet.Optional.EchoRequestClientTime, borrowed.Packet.Optional.EchoRequestClientTime); Assert.Equal( packet.Optional.FlowBytes, borrowed.Packet.Optional.FlowBytes); Assert.Equal( packet.Optional.FlowInterval, borrowed.Packet.Optional.FlowInterval); Assert.Equal( packet.Optional.ConnectRequestServerTime, borrowed.Packet.Optional.ConnectRequestServerTime); Assert.Equal( packet.Optional.ConnectRequestCookie, borrowed.Packet.Optional.ConnectRequestCookie); Assert.Equal( packet.Optional.ConnectRequestClientId, borrowed.Packet.Optional.ConnectRequestClientId); Assert.Equal( packet.Optional.ConnectRequestServerSeed, borrowed.Packet.Optional.ConnectRequestServerSeed); Assert.Equal( packet.Optional.ConnectRequestClientSeed, borrowed.Packet.Optional.ConnectRequestClientSeed); Assert.Equal( packet.Optional.RetransmitRequests, ReadRetransmits(borrowed.Packet.Optional)); var fragments = new List(); foreach (BorrowedMessageFragment fragment in borrowed.Packet.Fragments) { fragments.Add(fragment); } Assert.Equal(packet.Fragments.Count, borrowed.Packet.FragmentCount); Assert.Equal(packet.Fragments.Count, fragments.Count); for (int index = 0; index < fragments.Count; index++) { Assert.Equal( packet.Fragments[index].Header, fragments[index].Header); Assert.Equal( packet.Fragments[index].Payload, fragments[index].Payload.ToArray()); } } private static uint[] ReadRetransmits( BorrowedOptionalHeader optional) { var retransmits = new uint[optional.RetransmitRequestCount]; ReadOnlySpan bytes = optional.RetransmitRequestBytes.Span; for (int index = 0; index < retransmits.Length; index++) { retransmits[index] = BinaryPrimitives.ReadUInt32LittleEndian( bytes.Slice(index * 4)); } return retransmits; } private static void AssertSameError(byte[] datagram) { PacketCodec.PacketDecodeResult owned = PacketCodec.TryDecode(datagram, inboundIsaac: null); BorrowedPacketDecodeResult borrowed = PacketCodec.TryDecodeBorrowed( datagram, inboundIsaac: null); Assert.Equal(owned.Error, borrowed.Error); } private static byte[] Encode( PacketHeaderFlags flags, byte[] body, IsaacRandom? isaac = null) => PacketCodec.Encode( new PacketHeader { Sequence = 0x01020304, Flags = flags, Id = 0x0506, Time = 0x0708, Iteration = 0x090A, }, body, isaac); private static byte[] EncodeUnchecked( PacketHeaderFlags flags, byte[] body) { var header = new PacketHeader { Sequence = 0x01020304, Flags = flags, Id = 0x0506, Time = 0x0708, Iteration = 0x090A, DataSize = checked((ushort)body.Length), }; header.Checksum = header.CalculateHeaderHash32() + Hash32.Calculate(body); byte[] datagram = new byte[PacketHeader.Size + body.Length]; header.Pack(datagram); body.CopyTo(datagram, PacketHeader.Size); return datagram; } private static byte[] BuildFragment( uint sequence, ushort count, ushort index, ushort queue, byte[] payload) { var header = new MessageFragmentHeader { Sequence = sequence, Id = 0x80000000, Count = count, TotalSize = checked((ushort)( MessageFragmentHeader.Size + payload.Length)), Index = index, Queue = queue, }; byte[] wire = new byte[header.TotalSize]; header.Pack(wire); payload.CopyTo(wire, MessageFragmentHeader.Size); return wire; } private static void WriteUInt16( Span bytes, ref int position, ushort value) { BinaryPrimitives.WriteUInt16LittleEndian( bytes.Slice(position), value); position += sizeof(ushort); } private static void WriteUInt32( Span bytes, ref int position, uint value) { BinaryPrimitives.WriteUInt32LittleEndian( bytes.Slice(position), value); position += sizeof(uint); } private static void WriteUInt64( Span bytes, ref int position, ulong value) { BinaryPrimitives.WriteUInt64LittleEndian( bytes.Slice(position), value); position += sizeof(ulong); } private static void WriteSingle( Span bytes, ref int position, float value) { BinaryPrimitives.WriteSingleLittleEndian( bytes.Slice(position), value); position += sizeof(float); } private static void WriteDouble( Span bytes, ref int position, double value) { BinaryPrimitives.WriteInt64LittleEndian( bytes.Slice(position), BitConverter.DoubleToInt64Bits(value)); position += sizeof(double); } private static void Fill( Span bytes, ref int position, int count, byte value) { bytes.Slice(position, count).Fill(value); position += count; } }