using System.Buffers.Binary;
using AcDream.Core.Net.Cryptography;
using AcDream.Core.Net.Packets;
namespace AcDream.Core.Net.Tests.Packets;
///
/// The borrowed decode path, post-N2 split: TryParseBorrowed is the
/// pure parse + hash computation (no keystream anywhere) and
/// VerifyChecksum is the separate additive/encrypted comparison the
/// inbound sequence tracker keys.
///
public class BorrowedPacketCodecTests
{
[Fact]
public void TryParseBorrowed_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);
bool parsedOk = PacketCodec.TryParseBorrowed(
datagram,
out BorrowedPacket borrowed,
out uint headerHash,
out uint payloadHash,
out PacketCodec.DecodeError error);
Assert.True(parsedOk);
Assert.Equal(PacketCodec.DecodeError.None, error);
AssertEquivalent(owned, borrowed);
AssertCleartextChecksumHolds(borrowed, headerHash, payloadHash);
Assert.Equal(serverTime, borrowed.Optional.ConnectRequestServerTime);
Assert.Equal(cookie, borrowed.Optional.ConnectRequestCookie);
Assert.Equal(clientId, borrowed.Optional.ConnectRequestClientId);
Assert.Equal(serverSeed, borrowed.Optional.ConnectRequestServerSeed);
Assert.Equal(clientSeed, borrowed.Optional.ConnectRequestClientSeed);
Assert.Equal(timeSync, borrowed.Optional.TimeSync);
Assert.Equal(echoTime, borrowed.Optional.EchoRequestClientTime);
Assert.Equal(0xA0A1A2A3u, borrowed.Optional.FlowBytes);
Assert.Equal(0xB0B1, borrowed.Optional.FlowInterval);
Assert.Equal(
new uint[] { 0x10111213, 0x20212223 },
ReadIds(
borrowed.Optional.RetransmitRequestBytes,
borrowed.Optional.RetransmitRequestCount));
// N2: the RejectRetransmit ids are exposed on both decoders (they
// were always inside the hashed span; only the views are new).
Assert.Equal(
new uint[] { 0x30313233 },
ReadIds(
borrowed.Optional.RejectRetransmitBytes,
borrowed.Optional.RejectRetransmitCount));
Assert.Equal(
new uint[] { 0x30313233 },
owned.Packet!.Optional.RejectRetransmits);
}
[Fact]
public void TryParseBorrowed_LoginPayload_MatchesOwnedDecoder()
{
byte[] payload = LoginRequest.Build("borrowed", "packet", 47);
byte[] datagram = Encode(PacketHeaderFlags.LoginRequest, payload);
PacketCodec.PacketDecodeResult owned =
PacketCodec.TryDecode(datagram, inboundIsaac: null);
Assert.True(PacketCodec.TryParseBorrowed(
datagram,
out BorrowedPacket borrowed,
out uint headerHash,
out uint payloadHash,
out _));
AssertEquivalent(owned, borrowed);
AssertCleartextChecksumHolds(borrowed, headerHash, payloadHash);
Assert.Equal(payload, borrowed.Optional.RawBytes.ToArray());
Assert.Equal(payload, borrowed.Body.ToArray());
Assert.Equal(0, borrowed.FragmentCount);
}
[Fact]
public void VerifyChecksum_EncryptedForm_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));
Assert.True(owned.IsOk);
Assert.True(PacketCodec.TryParseBorrowed(
datagram,
out BorrowedPacket borrowed,
out uint headerHash,
out uint payloadHash,
out _));
AssertEquivalent(owned, borrowed);
// The parse never touched a keystream: the caller supplies the key.
PacketHeader header = borrowed.Header;
uint key = new IsaacRandom(seed).Next();
Assert.True(PacketCodec.VerifyChecksum(
in header, headerHash, payloadHash, key));
Assert.False(PacketCodec.VerifyChecksum(
in header, headerHash, payloadHash, key ^ 1u));
// The additive cleartext form must not accept an encrypted checksum.
Assert.False(PacketCodec.VerifyChecksum(
in header, headerHash, payloadHash, isaacKey: null));
}
[Fact]
public void TryParseBorrowed_MalformedPackets_MatchOwnedDecoderErrors()
{
byte[] shortHeader = new byte[PacketHeader.Size - 1];
AssertSameParseError(shortHeader);
var oversized = new byte[PacketHeader.Size + 2];
new PacketHeader { DataSize = 3 }.Pack(oversized);
AssertSameParseError(oversized);
byte[] shortOptional = EncodeUnchecked(
PacketHeaderFlags.TimeSync,
[0x01, 0x02, 0x03, 0x04]);
AssertSameParseError(shortOptional);
byte[] zeroCount = BuildFragment(
sequence: 1,
count: 0,
index: 0,
queue: 0,
payload: [0x01]);
AssertSameParseError(EncodeUnchecked(
PacketHeaderFlags.BlobFragments,
zeroCount));
byte[] invalidIndex = BuildFragment(
sequence: 1,
count: 1,
index: 1,
queue: 0,
payload: [0x01]);
AssertSameParseError(EncodeUnchecked(
PacketHeaderFlags.BlobFragments,
invalidIndex));
// A checksum mismatch is no longer a parse error: the parse
// succeeds and VerifyChecksum reports the failure (the owned
// decoder still folds it into DecodeError.ChecksumMismatch).
byte[] wrongChecksum = Encode(
PacketHeaderFlags.AckSequence,
[1, 2, 3, 4]);
wrongChecksum[8] ^= 0x80;
Assert.Equal(
PacketCodec.DecodeError.ChecksumMismatch,
PacketCodec.TryDecode(wrongChecksum, inboundIsaac: null).Error);
Assert.True(PacketCodec.TryParseBorrowed(
wrongChecksum,
out BorrowedPacket parsed,
out uint headerHash,
out uint payloadHash,
out PacketCodec.DecodeError error));
Assert.Equal(PacketCodec.DecodeError.None, error);
PacketHeader header = parsed.Header;
Assert.False(PacketCodec.VerifyChecksum(
in header, headerHash, payloadHash, isaacKey: null));
}
[Fact]
public void TryParseBorrowed_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)
{
if (!PacketCodec.TryParseBorrowed(
datagram,
out BorrowedPacket decoded,
out uint headerHash,
out uint payloadHash,
out _))
{
return 0;
}
PacketHeader header = decoded.Header;
if (!PacketCodec.VerifyChecksum(
in header, headerHash, payloadHash, isaacKey: null))
{
return 0;
}
int checksum = decoded.FragmentCount;
foreach (BorrowedMessageFragment fragment
in decoded.Fragments)
{
checksum += fragment.Header.TotalSize;
checksum += fragment.Payload.Span[0];
}
return checksum;
}
private static void AssertCleartextChecksumHolds(
in BorrowedPacket borrowed,
uint headerHash,
uint payloadHash)
{
PacketHeader header = borrowed.Header;
Assert.True(PacketCodec.VerifyChecksum(
in header, headerHash, payloadHash, isaacKey: null));
}
private static void AssertEquivalent(
PacketCodec.PacketDecodeResult owned,
in BorrowedPacket borrowed)
{
Assert.True(owned.IsOk);
Packet packet = Assert.IsType(owned.Packet);
Assert.Equal(packet.Header, borrowed.Header);
Assert.Equal(packet.BodyBytes, borrowed.Body.ToArray());
Assert.Equal(
packet.Optional.RawBytes,
borrowed.Optional.RawBytes.ToArray());
Assert.Equal(
packet.Optional.AckSequence,
borrowed.Optional.AckSequence);
Assert.Equal(
packet.Optional.TimeSync,
borrowed.Optional.TimeSync);
Assert.Equal(
packet.Optional.EchoRequestClientTime,
borrowed.Optional.EchoRequestClientTime);
Assert.Equal(
packet.Optional.FlowBytes,
borrowed.Optional.FlowBytes);
Assert.Equal(
packet.Optional.FlowInterval,
borrowed.Optional.FlowInterval);
Assert.Equal(
packet.Optional.ConnectRequestServerTime,
borrowed.Optional.ConnectRequestServerTime);
Assert.Equal(
packet.Optional.ConnectRequestCookie,
borrowed.Optional.ConnectRequestCookie);
Assert.Equal(
packet.Optional.ConnectRequestClientId,
borrowed.Optional.ConnectRequestClientId);
Assert.Equal(
packet.Optional.ConnectRequestServerSeed,
borrowed.Optional.ConnectRequestServerSeed);
Assert.Equal(
packet.Optional.ConnectRequestClientSeed,
borrowed.Optional.ConnectRequestClientSeed);
Assert.Equal(
packet.Optional.RetransmitRequests,
ReadIds(
borrowed.Optional.RetransmitRequestBytes,
borrowed.Optional.RetransmitRequestCount));
Assert.Equal(
packet.Optional.RejectRetransmits,
ReadIds(
borrowed.Optional.RejectRetransmitBytes,
borrowed.Optional.RejectRetransmitCount));
var fragments = new List();
foreach (BorrowedMessageFragment fragment
in borrowed.Fragments)
{
fragments.Add(fragment);
}
Assert.Equal(packet.Fragments.Count, borrowed.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[] ReadIds(
ReadOnlyMemory idMemory,
int count)
{
var ids = new uint[count];
ReadOnlySpan bytes = idMemory.Span;
for (int index = 0; index < ids.Length; index++)
{
ids[index] =
BinaryPrimitives.ReadUInt32LittleEndian(
bytes.Slice(index * 4));
}
return ids;
}
private static void AssertSameParseError(byte[] datagram)
{
PacketCodec.PacketDecodeResult owned =
PacketCodec.TryDecode(datagram, inboundIsaac: null);
Assert.False(PacketCodec.TryParseBorrowed(
datagram,
out _,
out _,
out _,
out PacketCodec.DecodeError borrowedError));
Assert.Equal(owned.Error, borrowedError);
}
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;
}
}