using System.Buffers.Binary; using System.Net.Sockets; namespace Ananta.SDK.Network; public static class FrameCodec { // Public/plain overload retained for handshake/debug compatibility. public static Task ReadFrameAsync(NetworkStream stream, CancellationToken token) => ReadFrameAsync(stream, null, token); internal static async Task ReadFrameAsync(NetworkStream stream, UxChaCha8? cryptor, CancellationToken token) { // UXStreamSocket decrypts the first five bytes (length + mode) as one // continuous stream, then decrypts the payload with the SAME cryptor state. var head = new byte[5]; if (!await ReadExactAsync(stream, head, token)) return null; var rawHead = Convert.ToHexString(head); cryptor?.XorInPlace(head); var payloadLength = BinaryPrimitives.ReadInt32LittleEndian(head.AsSpan(0, 4)); if (payloadLength < 0 || payloadLength > 8 * 1024 * 1024) { // Raw bytes kept on purpose: a frame sent before the peer installed // its cryptor arrives as plaintext and fails this check. Console.Error.WriteLine($"[FRAME-DBG] rawHead={rawHead} decryptedLen={payloadLength} mode={head[4]}"); throw new InvalidDataException($"Bad frame payload length after decrypt: {payloadLength}"); } var payload = new byte[payloadLength]; if (payloadLength != 0) { if (!await ReadExactAsync(stream, payload, token)) return null; var rawPayloadHead = Convert.ToHexString(payload.AsSpan(0, Math.Min(16, payload.Length))); cryptor?.XorInPlace(payload); Console.Error.WriteLine($"[FRAME-DBG] ok len={payloadLength} mode={head[4]} rawPayloadHead={rawPayloadHead} decHead={Convert.ToHexString(head)}"); } return new Frame(head[4], payload); } // Public/plain overload retained for the plaintext handshake reply. public static Task WriteFrameAsync(NetworkStream stream, byte mode, ReadOnlyMemory payload, CancellationToken token) => WriteFrameAsync(stream, mode, payload, null, token); internal static async Task WriteFrameAsync( NetworkStream stream, byte mode, ReadOnlyMemory payload, UxChaCha8? cryptor, CancellationToken token) { // Client Send() constructs [LE payloadLength][mode][payload] and, once // CryptorWrite exists, encrypts that full range in one continuous call. var frame = new byte[5 + payload.Length]; BinaryPrimitives.WriteInt32LittleEndian(frame.AsSpan(0, 4), payload.Length); frame[4] = mode; payload.Span.CopyTo(frame.AsSpan(5)); cryptor?.XorInPlace(frame); await stream.WriteAsync(frame, token); await stream.FlushAsync(token); } private static async Task ReadExactAsync(NetworkStream stream, byte[] buffer, CancellationToken token) { var offset = 0; while (offset < buffer.Length) { var read = await stream.ReadAsync(buffer.AsMemory(offset, buffer.Length - offset), token); if (read == 0) return false; offset += read; } return true; } }