101 lines
No EOL
3.8 KiB
C#
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);
|
|
}
|
|
} |