DataTransfer/DataTransfer.Client/Services/Frames.cs
2026-10-15 20:09:11 +02:00

101 lines
No EOL
3.8 KiB
C#

using System.Text;
namespace DataTransfer.Client.Services;
/// <summary>Application frames. They are encrypted as a whole, so the relay server cannot see their type or content.</summary>
public abstract record Frame;
public sealed record TextFrame(string Text) : Frame;
public sealed record FileStartFrame(int FileId, string Name, long Size) : Frame;
public sealed record FileChunkFrame(int FileId, ReadOnlyMemory<byte> Data) : Frame;
public sealed record FileCancelFrame(int FileId) : Frame;
/// <summary>Plaintext layout: version (byte) | type (byte) | sequence number (int64) | type-specific body.</summary>
internal static class FrameCodec
{
private const byte Version = 1;
private enum FrameType : byte
{
Text = 1,
FileStart = 2,
FileChunk = 3,
FileCancel = 4
}
public static byte[] Encode(long sequence, Frame frame)
{
var capacity = frame is FileChunkFrame chunk ? chunk.Data.Length + 32 : 256;
using var stream = new MemoryStream(capacity);
using (var writer = new BinaryWriter(stream, Encoding.UTF8, true))
{
writer.Write(Version);
switch (frame)
{
case TextFrame text:
WriteHeader(writer, FrameType.Text, sequence);
writer.Write(text.Text);
break;
case FileStartFrame start:
WriteHeader(writer, FrameType.FileStart, sequence);
writer.Write(start.FileId);
writer.Write(start.Name);
writer.Write(start.Size);
break;
case FileChunkFrame fileChunk:
WriteHeader(writer, FrameType.FileChunk, sequence);
writer.Write(fileChunk.FileId);
writer.Write(fileChunk.Data.Span);
break;
case FileCancelFrame cancel:
WriteHeader(writer, FrameType.FileCancel, sequence);
writer.Write(cancel.FileId);
break;
default:
throw new ArgumentOutOfRangeException(nameof(frame), frame, "Unknown frame.");
}
}
return stream.ToArray();
}
/// <exception cref="InvalidDataException">The plaintext is not a valid frame.</exception>
public static (long Sequence, Frame Frame) Decode(byte[] plaintext)
{
try
{
using var reader = new BinaryReader(new MemoryStream(plaintext, false), Encoding.UTF8);
if (reader.ReadByte() != Version)
{
throw new InvalidDataException("Unsupported frame version.");
}
var type = (FrameType)reader.ReadByte();
var sequence = reader.ReadInt64();
Frame frame = type switch
{
FrameType.Text => new TextFrame(reader.ReadString()),
FrameType.FileStart => new FileStartFrame(reader.ReadInt32(), reader.ReadString(), reader.ReadInt64()),
FrameType.FileChunk => new FileChunkFrame(reader.ReadInt32(), plaintext.AsMemory((int)reader.BaseStream.Position)),
FrameType.FileCancel => new FileCancelFrame(reader.ReadInt32()),
_ => throw new InvalidDataException("Unknown frame type.")
};
return (sequence, frame);
}
catch (Exception ex) when (ex is IOException or FormatException)
{
// EndOfStreamException (truncated), IOException (negative string length), FormatException (bad 7-bit length).
throw new InvalidDataException("Malformed frame.", ex);
}
}
private static void WriteHeader(BinaryWriter writer, FrameType type, long sequence)
{
writer.Write((byte)type);
writer.Write(sequence);
}
}