using System.Text; namespace DataTransfer.Client.Services; /// Application frames. They are encrypted as a whole, so the relay server cannot see their type or content. 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 Data) : Frame; public sealed record FileCancelFrame(int FileId) : Frame; /// Plaintext layout: version (byte) | type (byte) | sequence number (int64) | type-specific body. 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(); } /// The plaintext is not a valid frame. 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); } }