Decode headers, optional fields, fragments, and single-fragment messages directly over pooled datagrams. Copy only fragment state that crosses a datagram lifetime, preserve synchronous dispatch and ACK ordering, and lock the path to the owned decoder with differential and zero-allocation tests.
441 lines
14 KiB
C#
441 lines
14 KiB
C#
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<byte> 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<Packet>(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<BorrowedMessageFragment>();
|
|
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<byte> 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<byte> bytes,
|
|
ref int position,
|
|
ushort value)
|
|
{
|
|
BinaryPrimitives.WriteUInt16LittleEndian(
|
|
bytes.Slice(position),
|
|
value);
|
|
position += sizeof(ushort);
|
|
}
|
|
|
|
private static void WriteUInt32(
|
|
Span<byte> bytes,
|
|
ref int position,
|
|
uint value)
|
|
{
|
|
BinaryPrimitives.WriteUInt32LittleEndian(
|
|
bytes.Slice(position),
|
|
value);
|
|
position += sizeof(uint);
|
|
}
|
|
|
|
private static void WriteUInt64(
|
|
Span<byte> bytes,
|
|
ref int position,
|
|
ulong value)
|
|
{
|
|
BinaryPrimitives.WriteUInt64LittleEndian(
|
|
bytes.Slice(position),
|
|
value);
|
|
position += sizeof(ulong);
|
|
}
|
|
|
|
private static void WriteSingle(
|
|
Span<byte> bytes,
|
|
ref int position,
|
|
float value)
|
|
{
|
|
BinaryPrimitives.WriteSingleLittleEndian(
|
|
bytes.Slice(position),
|
|
value);
|
|
position += sizeof(float);
|
|
}
|
|
|
|
private static void WriteDouble(
|
|
Span<byte> bytes,
|
|
ref int position,
|
|
double value)
|
|
{
|
|
BinaryPrimitives.WriteInt64LittleEndian(
|
|
bytes.Slice(position),
|
|
BitConverter.DoubleToInt64Bits(value));
|
|
position += sizeof(double);
|
|
}
|
|
|
|
private static void Fill(
|
|
Span<byte> bytes,
|
|
ref int position,
|
|
int count,
|
|
byte value)
|
|
{
|
|
bytes.Slice(position, count).Fill(value);
|
|
position += count;
|
|
}
|
|
}
|