using System.Buffers.Binary;
using StellaOps.Router.Common.Enums;
using StellaOps.Router.Common.Models;
namespace StellaOps.Router.Transport.Tcp;
///
/// Handles reading and writing length-prefixed frames over a stream.
/// Frame format: [4-byte big-endian length][payload]
/// Payload format: [1-byte frame type][16-byte correlation GUID][remaining data]
///
public static class FrameProtocol
{
private const int LengthPrefixSize = 4;
private const int FrameTypeSize = 1;
private const int CorrelationIdSize = 16;
private const int HeaderSize = FrameTypeSize + CorrelationIdSize;
///
/// Reads a complete frame from the stream.
///
/// The stream to read from.
/// The maximum frame size allowed.
/// Cancellation token.
/// The frame read, or null if the stream is closed.
public static async Task ReadFrameAsync(
Stream stream,
int maxFrameSize,
CancellationToken cancellationToken)
{
// Read length prefix (4 bytes, big-endian)
var lengthBuffer = new byte[LengthPrefixSize];
var bytesRead = await ReadExactAsync(stream, lengthBuffer, cancellationToken);
if (bytesRead == 0)
{
return null; // Connection closed
}
if (bytesRead < LengthPrefixSize)
{
throw new InvalidOperationException("Incomplete length prefix received");
}
var payloadLength = BinaryPrimitives.ReadInt32BigEndian(lengthBuffer);
if (payloadLength < HeaderSize)
{
throw new InvalidOperationException($"Invalid payload length: {payloadLength}");
}
if (payloadLength > maxFrameSize)
{
throw new InvalidOperationException(
$"Frame size {payloadLength} exceeds maximum {maxFrameSize}");
}
// Read payload
var payload = new byte[payloadLength];
bytesRead = await ReadExactAsync(stream, payload, cancellationToken);
if (bytesRead < payloadLength)
{
throw new InvalidOperationException(
$"Incomplete payload: expected {payloadLength}, got {bytesRead}");
}
// Parse frame
var frameType = (FrameType)payload[0];
var correlationId = new Guid(payload.AsSpan(FrameTypeSize, CorrelationIdSize));
var data = payload.AsMemory(HeaderSize);
return new Frame
{
Type = frameType,
CorrelationId = correlationId.ToString("N"),
Payload = data
};
}
///
/// Writes a frame to the stream.
///
/// The stream to write to.
/// The frame to write.
/// Cancellation token.
public static async Task WriteFrameAsync(
Stream stream,
Frame frame,
CancellationToken cancellationToken)
{
// Parse or generate correlation ID
var correlationGuid = frame.CorrelationId is not null &&
Guid.TryParse(frame.CorrelationId, out var parsed)
? parsed
: Guid.NewGuid();
var dataLength = frame.Payload.Length;
var payloadLength = HeaderSize + dataLength;
// Create buffer for the complete message
var buffer = new byte[LengthPrefixSize + payloadLength];
// Write length prefix (big-endian)
BinaryPrimitives.WriteInt32BigEndian(buffer.AsSpan(0, LengthPrefixSize), payloadLength);
// Write frame type
buffer[LengthPrefixSize] = (byte)frame.Type;
// Write correlation ID
correlationGuid.TryWriteBytes(buffer.AsSpan(LengthPrefixSize + FrameTypeSize, CorrelationIdSize));
// Write data
if (dataLength > 0)
{
frame.Payload.Span.CopyTo(buffer.AsSpan(LengthPrefixSize + HeaderSize));
}
await stream.WriteAsync(buffer, cancellationToken);
}
///
/// Reads exactly the specified number of bytes from the stream.
///
private static async Task ReadExactAsync(
Stream stream,
Memory buffer,
CancellationToken cancellationToken)
{
var totalRead = 0;
while (totalRead < buffer.Length)
{
var read = await stream.ReadAsync(
buffer[totalRead..],
cancellationToken);
if (read == 0)
{
return totalRead; // EOF
}
totalRead += read;
}
return totalRead;
}
}