acdream/tests/AcDream.Core.Net.Tests/Packets/PacketWriterTests.cs
Erik 44c335469a feat(net): PacketWriter + LoginRequest payload builder (Phase 4.5a/b)
Adds the outbound-side primitives acdream needs to send a LoginRequest
packet to an ACE server: a growable byte buffer writer with AC's two
length-prefixed string formats, plus the LoginRequest payload builder
and parser.

PacketWriter (Packets/PacketWriter.cs):
  - Growable byte[] buffer with little-endian WriteByte/UInt16/UInt32/Bytes
  - WriteString16L: u16 length + ASCII bytes + zero-pad to 4-byte boundary
    (pad counted from start of length prefix, matching ACE's Extensions.cs)
  - WriteString32L: u32 outer length (= asciiLen+1) + 1 marker byte (value
    ignored by reader, we emit 0) + ASCII + pad. Reader decrements the
    outer length by 1 when consuming the marker, so asciiLen is recovered
    correctly. Asserts ≤255 chars (two-byte-marker variant not needed for
    acdream's dev credentials).
  - ASCII encoding used instead of Windows-1252 since dev account names
    and passwords are ASCII-safe; can switch to CodePages later if a
    non-ASCII identifier ever turns up.

LoginRequest (Packets/LoginRequest.cs):
  - Build(account, password, timestamp, clientVersion="1802") produces
    the login payload bytes that go into the body of a packet whose
    header has the LoginRequest flag set
  - Parse(bytes) for tests and diagnostics — server never calls this
    in production, but round-trip tests make the writer self-verifying
  - NetAuthType enum mirrors ACE: Account/AccountPassword/GlsTicket
  - Wire layout per ACE's PacketInboundLoginRequest:
      String16L ClientVersion
      u32       bodyLength (bytes remaining after this field)
      u32       NetAuthType (2 = AccountPassword)
      u32       AuthFlags (0 for normal client)
      u32       Timestamp
      String16L Account
      String16L LoginAs (empty for non-admin)
      String32L Password (when AccountPassword)
  - bodyLength field is back-patched after the full body has been
    written (classic "write placeholder, come back and patch" flow)

Tests (17 new, 61 total in net project, 138 across both test projects):
  PacketWriter (11):
    - u32 little-endian
    - String16L: empty, 1/2/3-char with correct padding
    - String32L: 2-char short, empty, >255 throws
    - AlignTo4 no-op when aligned, pads when not
    - Buffer grows past initial capacity on big writes
  LoginRequest (6):
    - Build→Parse round-trip with realistic credentials (testaccount/
      testpassword/timestamp)
    - Empty account/password round-trip (padding edge case)
    - BodyLength field reflects actual remaining bytes after itself
    - Total wire size is multiple of 4 (sanity check on padding)
    - Different credentials produce different bytes
    - End-to-end: payload embedded in a full Packet with LoginRequest
      header flag + correct unencrypted checksum, PacketCodec.TryDecode
      parses it, BodyBytes round-trips back to the same credentials
      through LoginRequest.Parse

This gives acdream everything needed to construct the first datagram
of the handshake. Phase 4.5c next: WorldSession state machine to drive
the handshake sequence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:36:39 +02:00

120 lines
3.5 KiB
C#

using AcDream.Core.Net.Packets;
namespace AcDream.Core.Net.Tests.Packets;
public class PacketWriterTests
{
[Fact]
public void WriteUInt32_WritesLittleEndianBytes()
{
var w = new PacketWriter();
w.WriteUInt32(0x04030201u);
Assert.Equal(new byte[] { 0x01, 0x02, 0x03, 0x04 }, w.ToArray());
}
[Fact]
public void WriteString16L_EmptyString_WritesOnlyLengthZeroAndPadding()
{
var w = new PacketWriter();
w.WriteString16L(string.Empty);
// u16(0) = 2 bytes, pad to 4 = 2 more bytes
Assert.Equal(new byte[] { 0x00, 0x00, 0x00, 0x00 }, w.ToArray());
}
[Fact]
public void WriteString16L_TwoCharString_PadsToMultipleOfFour()
{
var w = new PacketWriter();
w.WriteString16L("ab");
// u16(2), 'a', 'b', 0 pad byte → total 5 bytes... wait that's not 4-aligned.
// Record = 2 (length prefix) + 2 (ascii) = 4. No padding needed.
Assert.Equal(new byte[] { 0x02, 0x00, (byte)'a', (byte)'b' }, w.ToArray());
}
[Fact]
public void WriteString16L_OneCharString_PadsToFour()
{
var w = new PacketWriter();
w.WriteString16L("x");
// Record = 2 + 1 = 3, pad 1 byte → 4 total
Assert.Equal(new byte[] { 0x01, 0x00, (byte)'x', 0x00 }, w.ToArray());
}
[Fact]
public void WriteString16L_ThreeCharString_PadsToEight()
{
var w = new PacketWriter();
w.WriteString16L("abc");
// Record = 2 + 3 = 5, pad 3 → 8 total
Assert.Equal(new byte[] { 0x03, 0x00, (byte)'a', (byte)'b', (byte)'c', 0, 0, 0 }, w.ToArray());
}
[Fact]
public void WriteString32L_ShortString_WritesU32OuterMarkerStringPadded()
{
var w = new PacketWriter();
w.WriteString32L("hi");
// outer = 2 + 1 = 3, then marker byte 0, then 'h' 'i', pad from start
// of u32: record = 4 + 1 + 2 = 7, pad 1 → 8 total
Assert.Equal(new byte[]
{
0x03, 0x00, 0x00, 0x00, // outer length = 3
0x00, // marker
(byte)'h', (byte)'i',
0x00, // pad to 8
}, w.ToArray());
}
[Fact]
public void WriteString32L_EmptyString_WritesOnlyZeroLength()
{
var w = new PacketWriter();
w.WriteString32L("");
Assert.Equal(new byte[] { 0x00, 0x00, 0x00, 0x00 }, w.ToArray());
}
[Fact]
public void WriteString32L_TooLong_Throws()
{
var w = new PacketWriter();
Assert.Throws<ArgumentException>(() => w.WriteString32L(new string('x', 256)));
}
[Fact]
public void AlignTo4_NoOpIfAlreadyAligned()
{
var w = new PacketWriter();
w.WriteUInt32(0xCAFEu);
int before = w.Position;
w.AlignTo4();
Assert.Equal(before, w.Position);
}
[Fact]
public void AlignTo4_AddsPaddingWhenMisaligned()
{
var w = new PacketWriter();
w.WriteByte(0xAA);
w.AlignTo4();
Assert.Equal(4, w.Position);
var buf = w.ToArray();
Assert.Equal(0xAA, buf[0]);
Assert.Equal(0x00, buf[1]);
Assert.Equal(0x00, buf[2]);
Assert.Equal(0x00, buf[3]);
}
[Fact]
public void Grow_HandlesLargeWrites()
{
var w = new PacketWriter(16); // small initial
var big = new byte[500];
for (int i = 0; i < big.Length; i++) big[i] = (byte)(i & 0xFF);
w.WriteBytes(big);
Assert.Equal(500, w.Position);
Assert.Equal(big, w.ToArray());
}
}