mirror of
https://seed.radicle.dev/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5.git
synced 2026-11-16 00:00:37 +08:00
This required adding a type parameter to `Frame` to be able to test different message types.
396 lines
12 KiB
Rust
396 lines
12 KiB
Rust
//! Framing protocol.
|
|
#![warn(clippy::missing_docs_in_private_items)]
|
|
use std::{fmt, io};
|
|
|
|
use crate::{wire, wire::varint, wire::varint::VarInt, wire::Message, Link, PROTOCOL_VERSION};
|
|
|
|
/// Protocol version strings all start with the magic sequence `rad`, followed
|
|
/// by a version number.
|
|
pub const PROTOCOL_VERSION_STRING: Version = Version([b'r', b'a', b'd', PROTOCOL_VERSION]);
|
|
|
|
/// Control open byte.
|
|
const CONTROL_OPEN: u8 = 0;
|
|
/// Control close byte.
|
|
const CONTROL_CLOSE: u8 = 1;
|
|
/// Control EOF byte.
|
|
const CONTROL_EOF: u8 = 2;
|
|
|
|
/// Protocol version.
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub struct Version([u8; 4]);
|
|
|
|
impl Version {
|
|
/// Version number.
|
|
pub fn number(&self) -> u8 {
|
|
self.0[3]
|
|
}
|
|
}
|
|
|
|
impl wire::Encode for Version {
|
|
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
|
|
writer.write_all(&PROTOCOL_VERSION_STRING.0)?;
|
|
|
|
Ok(PROTOCOL_VERSION_STRING.0.len())
|
|
}
|
|
}
|
|
|
|
impl wire::Decode for Version {
|
|
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
|
|
let mut version = [0u8; 4];
|
|
reader.read_exact(&mut version[..])?;
|
|
|
|
if version != PROTOCOL_VERSION_STRING.0 {
|
|
return Err(wire::Error::InvalidProtocolVersion(version));
|
|
}
|
|
Ok(Self(version))
|
|
}
|
|
}
|
|
|
|
/// Identifies a (multiplexed) stream.
|
|
///
|
|
/// Stream IDs are variable-length integers with the least significant 3 bits
|
|
/// denoting the stream type and initiator.
|
|
///
|
|
/// The first bit denotes the initiator (outbound or inbound), while the second
|
|
/// and third bit denote the stream type. See `StreamKind`.
|
|
///
|
|
/// In a situation where Alice connects to Bob, Alice will have the initiator
|
|
/// bit set to `1` for all streams she creates, while Bob will have it set to `0`.
|
|
///
|
|
/// This ensures that Stream IDs never collide.
|
|
/// Additionally, Stream IDs must never be re-used within a connection.
|
|
///
|
|
/// +=======+==================================+
|
|
/// | Bits | Stream Type |
|
|
/// +=======+==================================+
|
|
/// | 0b000 | Outbound Control stream |
|
|
/// +-------+----------------------------------+
|
|
/// | 0b001 | Inbound Control stream |
|
|
/// +-------+----------------------------------+
|
|
/// | 0b010 | Outbound Gossip stream |
|
|
/// +-------+----------------------------------+
|
|
/// | 0b011 | Inbound Gossip stream |
|
|
/// +-------+----------------------------------+
|
|
/// | 0b100 | Outbound Git stream |
|
|
/// +-------+----------------------------------+
|
|
/// | 0b101 | Inbound Git stream |
|
|
/// +-------+----------------------------------+
|
|
///
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub struct StreamId(VarInt);
|
|
|
|
impl StreamId {
|
|
/// Get the initiator of this stream.
|
|
pub fn link(&self) -> Link {
|
|
let n = *self.0;
|
|
if 0b1 & n == 0 {
|
|
Link::Outbound
|
|
} else {
|
|
Link::Inbound
|
|
}
|
|
}
|
|
|
|
/// Get the kind of stream this is.
|
|
pub fn kind(&self) -> Result<StreamKind, u8> {
|
|
let id = *self.0;
|
|
let kind = ((id >> 1) & 0b11) as u8;
|
|
|
|
StreamKind::try_from(kind)
|
|
}
|
|
|
|
/// Create a control identifier.
|
|
pub fn control(link: Link) -> Self {
|
|
let link = if link.is_outbound() { 0 } else { 1 };
|
|
Self(VarInt::from((StreamKind::Control as u8) << 1 | link))
|
|
}
|
|
|
|
/// Create a gossip identifier.
|
|
pub fn gossip(link: Link) -> Self {
|
|
let link = if link.is_outbound() { 0 } else { 1 };
|
|
Self(VarInt::from((StreamKind::Gossip as u8) << 1 | link))
|
|
}
|
|
|
|
/// Create a git identifier.
|
|
pub fn git(link: Link) -> Self {
|
|
let link = if link.is_outbound() { 0 } else { 1 };
|
|
Self(VarInt::from((StreamKind::Git as u8) << 1 | link))
|
|
}
|
|
|
|
/// Get the nth identifier while preserving the stream type and initiator.
|
|
pub fn nth(self, n: u64) -> Result<Self, varint::BoundsExceeded> {
|
|
let id = *self.0 + (n << 3);
|
|
VarInt::new(id).map(Self)
|
|
}
|
|
}
|
|
|
|
impl From<StreamId> for u64 {
|
|
fn from(value: StreamId) -> Self {
|
|
*value.0
|
|
}
|
|
}
|
|
|
|
impl From<StreamId> for VarInt {
|
|
fn from(value: StreamId) -> Self {
|
|
value.0
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for StreamId {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", *self.0)
|
|
}
|
|
}
|
|
|
|
impl wire::Decode for StreamId {
|
|
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
|
|
let id = VarInt::decode(reader)?;
|
|
Ok(Self(id))
|
|
}
|
|
}
|
|
|
|
impl wire::Encode for StreamId {
|
|
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
|
|
self.0.encode(writer)
|
|
}
|
|
}
|
|
|
|
/// Type of stream.
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
|
#[repr(u8)]
|
|
pub enum StreamKind {
|
|
/// Control stream, used to open and close streams.
|
|
Control = 0b00,
|
|
/// Gossip stream, used to exchange messages.
|
|
Gossip = 0b01,
|
|
/// Git stream, used for replication.
|
|
Git = 0b10,
|
|
}
|
|
|
|
impl TryFrom<u8> for StreamKind {
|
|
type Error = u8;
|
|
|
|
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
|
match value {
|
|
0b00 => Ok(StreamKind::Control),
|
|
0b01 => Ok(StreamKind::Gossip),
|
|
0b10 => Ok(StreamKind::Git),
|
|
n => Err(n),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Protocol frame.
|
|
///
|
|
/// 0 1 2 3
|
|
/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
/// | 'r' | 'a' | 'd' | 0x1 | Version
|
|
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
/// | Stream ID |TTT|I| Stream ID with Stream [T]ype and [I]nitiator bits
|
|
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
/// | Data ...| Data (variable size)
|
|
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub struct Frame<M = Message> {
|
|
/// The protocol version.
|
|
pub version: Version,
|
|
/// The stream identifier.
|
|
pub stream: StreamId,
|
|
/// The frame payload.
|
|
pub data: FrameData<M>,
|
|
}
|
|
|
|
impl<M> Frame<M> {
|
|
/// Create a 'git' protocol frame.
|
|
pub fn git(stream: StreamId, data: Vec<u8>) -> Self {
|
|
Self {
|
|
version: PROTOCOL_VERSION_STRING,
|
|
stream,
|
|
data: FrameData::Git(data),
|
|
}
|
|
}
|
|
|
|
/// Create a 'control' protocol frame.
|
|
pub fn control(link: Link, ctrl: Control) -> Self {
|
|
Self {
|
|
version: PROTOCOL_VERSION_STRING,
|
|
stream: StreamId::control(link),
|
|
data: FrameData::Control(ctrl),
|
|
}
|
|
}
|
|
|
|
/// Create a 'gossip' protocol frame.
|
|
pub fn gossip(link: Link, msg: M) -> Self {
|
|
Self {
|
|
version: PROTOCOL_VERSION_STRING,
|
|
stream: StreamId::gossip(link),
|
|
data: FrameData::Gossip(msg),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<M: wire::Encode> Frame<M> {
|
|
/// Serialize frame to bytes.
|
|
pub fn to_bytes(&self) -> Vec<u8> {
|
|
wire::serialize(self)
|
|
}
|
|
}
|
|
|
|
/// Frame payload.
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub enum FrameData<M> {
|
|
/// Control frame payload.
|
|
Control(Control),
|
|
/// Gossip frame payload.
|
|
Gossip(M),
|
|
/// Git frame payload. May contain packet-lines as well as packfile data.
|
|
Git(Vec<u8>),
|
|
}
|
|
|
|
/// A control message sent over a control stream.
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub enum Control {
|
|
/// Open a new stream.
|
|
Open {
|
|
/// The stream to open.
|
|
stream: StreamId,
|
|
},
|
|
/// Close an existing stream.
|
|
Close {
|
|
/// The stream to close.
|
|
stream: StreamId,
|
|
},
|
|
/// Signal an end-of-file. This can be used to simulate connections terminating
|
|
/// without having to close the connection. These control messages are turned into
|
|
/// [`io::ErrorKind::UnexpectedEof`] errors on read.
|
|
Eof {
|
|
/// The stream to send an EOF on.
|
|
stream: StreamId,
|
|
},
|
|
}
|
|
|
|
impl wire::Decode for Control {
|
|
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
|
|
let command = u8::decode(reader)?;
|
|
match command {
|
|
CONTROL_OPEN => {
|
|
let stream = StreamId::decode(reader)?;
|
|
Ok(Control::Open { stream })
|
|
}
|
|
CONTROL_CLOSE => {
|
|
let stream = StreamId::decode(reader)?;
|
|
Ok(Control::Close { stream })
|
|
}
|
|
CONTROL_EOF => {
|
|
let stream = StreamId::decode(reader)?;
|
|
Ok(Control::Eof { stream })
|
|
}
|
|
other => Err(wire::Error::InvalidControlMessage(other)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl wire::Encode for Control {
|
|
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
|
|
let mut n = 0;
|
|
|
|
match self {
|
|
Self::Open { stream: id } => {
|
|
n += CONTROL_OPEN.encode(writer)?;
|
|
n += id.encode(writer)?;
|
|
}
|
|
Self::Eof { stream: id } => {
|
|
n += CONTROL_EOF.encode(writer)?;
|
|
n += id.encode(writer)?;
|
|
}
|
|
Self::Close { stream: id } => {
|
|
n += CONTROL_CLOSE.encode(writer)?;
|
|
n += id.encode(writer)?;
|
|
}
|
|
}
|
|
Ok(n)
|
|
}
|
|
}
|
|
|
|
impl<M: wire::Decode> wire::Decode for Frame<M> {
|
|
fn decode<R: io::Read + ?Sized>(reader: &mut R) -> Result<Self, wire::Error> {
|
|
let version = Version::decode(reader)?;
|
|
if version.number() != PROTOCOL_VERSION {
|
|
return Err(wire::Error::WrongProtocolVersion(version.number()));
|
|
}
|
|
let stream = StreamId::decode(reader)?;
|
|
|
|
match stream.kind() {
|
|
Ok(StreamKind::Control) => {
|
|
let ctrl = Control::decode(reader)?;
|
|
let frame = Frame {
|
|
version,
|
|
stream,
|
|
data: FrameData::Control(ctrl),
|
|
};
|
|
Ok(frame)
|
|
}
|
|
Ok(StreamKind::Gossip) => {
|
|
let data = varint::payload::decode(reader)?;
|
|
let mut cursor = io::Cursor::new(data);
|
|
let msg = M::decode(&mut cursor)?;
|
|
let frame = Frame {
|
|
version,
|
|
stream,
|
|
data: FrameData::Gossip(msg),
|
|
};
|
|
|
|
// Nb. If there is data after the `Message` that is not decoded,
|
|
// it is simply dropped here.
|
|
|
|
Ok(frame)
|
|
}
|
|
Ok(StreamKind::Git { .. }) => {
|
|
let data = varint::payload::decode(reader)?;
|
|
Ok(Frame::git(stream, data))
|
|
}
|
|
Err(n) => Err(wire::Error::InvalidStreamKind(n)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<M: wire::Encode> wire::Encode for Frame<M> {
|
|
fn encode<W: io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, io::Error> {
|
|
let mut n = 0;
|
|
|
|
n += self.version.encode(writer)?;
|
|
n += self.stream.encode(writer)?;
|
|
n += match &self.data {
|
|
FrameData::Control(ctrl) => ctrl.encode(writer)?,
|
|
FrameData::Git(data) => varint::payload::encode(data, writer)?,
|
|
FrameData::Gossip(msg) => varint::payload::encode(&wire::serialize(msg), writer)?,
|
|
};
|
|
|
|
Ok(n)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_stream_id() {
|
|
assert_eq!(StreamId(VarInt(0b000)).kind().unwrap(), StreamKind::Control);
|
|
assert_eq!(StreamId(VarInt(0b010)).kind().unwrap(), StreamKind::Gossip);
|
|
assert_eq!(StreamId(VarInt(0b100)).kind().unwrap(), StreamKind::Git);
|
|
assert_eq!(StreamId(VarInt(0b001)).link(), Link::Inbound);
|
|
assert_eq!(StreamId(VarInt(0b000)).link(), Link::Outbound);
|
|
assert_eq!(StreamId(VarInt(0b101)).link(), Link::Inbound);
|
|
assert_eq!(StreamId(VarInt(0b100)).link(), Link::Outbound);
|
|
|
|
assert_eq!(StreamId::git(Link::Outbound), StreamId(VarInt(0b100)));
|
|
assert_eq!(StreamId::control(Link::Outbound), StreamId(VarInt(0b000)));
|
|
assert_eq!(StreamId::gossip(Link::Outbound), StreamId(VarInt(0b010)));
|
|
|
|
assert_eq!(StreamId::git(Link::Inbound), StreamId(VarInt(0b101)));
|
|
assert_eq!(StreamId::control(Link::Inbound), StreamId(VarInt(0b001)));
|
|
assert_eq!(StreamId::gossip(Link::Inbound), StreamId(VarInt(0b011)));
|
|
}
|
|
}
|