Adds the 20-byte AC UDP packet header struct + pack/unpack + its
checksum helper, and the Hash32 primitive the checksum uses.
Hash32 (Cryptography/Hash32.cs):
- Seeds accumulator with length << 16
- Sums input as little-endian uint32s word-aligned
- Folds any trailing 1-3 bytes via descending shift (24 → 16 → 8)
- Hand-computed golden values for 4-byte, 5-byte, and each 1/2/3
tail-byte case — no oracle needed, algorithm is simple enough to
verify by tracing
PacketHeader (Packets/PacketHeader.cs):
- Pack/Unpack: Sequence, Flags, Checksum, Id, Time, DataSize, Iteration
(20 bytes, little-endian on the wire)
- CalculateHeaderHash32: substitutes the 0xBADD70DD sentinel for the
Checksum field before hashing (matches AC retail + ACE convention —
without it the checksum would chicken-and-egg on itself). Uses a
local struct copy so the real Checksum isn't mutated on the caller.
- HasFlag for bitmask queries
PacketHeaderFlags (Packets/PacketHeaderFlags.cs):
- Full flag enum from ACE reference: Retransmission, EncryptedChecksum,
BlobFragments, ServerSwitch, ConnectRequest/Response, LoginRequest,
AckSequence, TimeSync, Disconnect, NetError, EchoRequest/Response,
Flow, and friends
Tests (15 new, 20 total in net project, 97 across both projects):
Hash32 (7):
- Empty returns 0
- 4-byte known value (hand-computed from bit layout)
- 5-byte value with one tail byte
- 1/2/3 tail-byte boundary cases (verifies 24/16/8 shift ordering)
- Determinism
PacketHeader (8):
- Pack/Unpack round-trip preserving all 7 fields
- Pack writes little-endian wire format in byte order
- HasFlag single and multi-bit
- CalculateHeaderHash32 invariance under Checksum field changes
(the critical property — verifies the BADD sentinel substitution)
- CalculateHeaderHash32 doesn't mutate
- CalculateHeaderHash32 determinism
- Unpack/Pack size-check throw
User confirmed an ACE server is running on localhost for the future
Phase 4.6 live integration step. Credentials will be read from env
vars at runtime, never committed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
41 lines
1.4 KiB
C#
41 lines
1.4 KiB
C#
using System.Buffers.Binary;
|
|
|
|
namespace AcDream.Core.Net.Cryptography;
|
|
|
|
/// <summary>
|
|
/// Simple 32-bit checksum used by the AC packet header. The algorithm is:
|
|
/// <list type="bullet">
|
|
/// <item>Seed the accumulator with <c>length << 16</c>.</item>
|
|
/// <item>Sum the input as little-endian uint32s, four bytes at a time.</item>
|
|
/// <item>Fold any tail bytes (length not divisible by 4) into the top of
|
|
/// the accumulator with a descending left-shift per byte (24, 16, 8, 0).</item>
|
|
/// </list>
|
|
/// Reimplemented from reading ACE's AGPL reference
|
|
/// (<c>references/ACE/Source/ACE.Common/Cryptography/Hash32.cs</c>). See
|
|
/// <c>NOTICE.md</c> for the attribution policy.
|
|
/// </summary>
|
|
public static class Hash32
|
|
{
|
|
public static uint Calculate(ReadOnlySpan<byte> data)
|
|
{
|
|
int length = data.Length;
|
|
uint checksum = (uint)length << 16;
|
|
|
|
int wordAligned = length & ~3;
|
|
for (int i = 0; i < wordAligned; i += 4)
|
|
{
|
|
checksum += BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(i));
|
|
}
|
|
|
|
// Tail bytes (0, 1, 2, or 3 bytes) with descending shift: first tail
|
|
// byte goes into bits 24..31, second into 16..23, third into 8..15.
|
|
int shift = 24;
|
|
for (int j = wordAligned; j < length; j++)
|
|
{
|
|
checksum += (uint)data[j] << shift;
|
|
shift -= 8;
|
|
}
|
|
|
|
return checksum;
|
|
}
|
|
}
|