use bytes::{BufMut, BytesMut};
#[cfg(test)]
use std::cmp::Ordering;
use std::fmt;
use std::iter::FusedIterator;
use std::ops::Range;
pub(crate) const MUX_VERSION: u8 = 1;
pub(crate) const BATCH_HEADER_LEN: usize = 16;
pub(crate) const RECORD_HEADER_LEN: usize = 13;
pub(crate) const MAX_RECORDS_PER_BATCH: u16 = u16::MAX;
pub(crate) const MAX_SLOT_INDEX: u32 = 0x00FF_FFFF;
fn read_u8(src: &[u8], at: usize) -> Option<u8> {
src.get(at).copied()
}
fn read_u16(src: &[u8], at: usize) -> Option<u16> {
let end = at.checked_add(2)?;
let raw: [u8; 2] = src.get(at..end)?.try_into().ok()?;
Some(u16::from_be_bytes(raw))
}
fn read_u32(src: &[u8], at: usize) -> Option<u32> {
let end = at.checked_add(4)?;
let raw: [u8; 4] = src.get(at..end)?.try_into().ok()?;
Some(u32::from_be_bytes(raw))
}
fn read_u64(src: &[u8], at: usize) -> Option<u64> {
let end = at.checked_add(8)?;
let raw: [u8; 8] = src.get(at..end)?.try_into().ok()?;
Some(u64::from_be_bytes(raw))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct BatchHeader {
pub(crate) mux_version: u8,
pub(crate) flags: u8,
pub(crate) record_count: u16,
pub(crate) peer_epoch: u64,
pub(crate) batch_seq: u32,
}
impl BatchHeader {
pub(crate) const fn new(peer_epoch: u64, batch_seq: u32) -> Self {
Self {
mux_version: MUX_VERSION,
flags: 0,
record_count: 0,
peer_epoch,
batch_seq,
}
}
pub(crate) const fn is_supported(&self) -> bool {
self.mux_version == MUX_VERSION
}
pub(crate) fn encode_into(&self, out: &mut BytesMut) {
out.put_u8(self.mux_version);
out.put_u8(self.flags);
out.put_u16(self.record_count);
out.put_u64(self.peer_epoch);
out.put_u32(self.batch_seq);
}
pub(crate) fn decode(src: &[u8]) -> Result<Self, DecodeError> {
let (Some(mux_version), Some(flags), Some(record_count), Some(peer_epoch), Some(batch_seq)) = (
read_u8(src, 0),
read_u8(src, 1),
read_u16(src, 2),
read_u64(src, 4),
read_u32(src, 12),
) else {
return Err(DecodeError::TruncatedBatchHeader { len: src.len() });
};
Ok(Self {
mux_version,
flags,
record_count,
peer_epoch,
batch_seq,
})
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub(crate) struct SlotId(u32);
impl SlotId {
pub(crate) const fn new(index: u32, generation: u8) -> Option<Self> {
if index > MAX_SLOT_INDEX {
return None;
}
Some(Self((index << 8) | generation as u32))
}
pub(crate) const fn from_raw(raw: u32) -> Self {
Self(raw)
}
pub(crate) const fn raw(self) -> u32 {
self.0
}
pub(crate) const fn index(self) -> u32 {
self.0 >> 8
}
pub(crate) const fn generation(self) -> u8 {
(self.0 & 0xFF) as u8
}
#[cfg(test)]
pub(crate) const fn with_generation(self, generation: u8) -> Self {
Self((self.0 & !0xFF) | generation as u32)
}
#[cfg(test)]
pub(crate) const fn next_generation(self) -> Self {
self.with_generation(self.generation().wrapping_add(1))
}
}
impl fmt::Debug for SlotId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SlotId")
.field("index", &self.index())
.field("generation", &self.generation())
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub(crate) enum RecordType {
Data = 0,
OpenSlot = 1,
CloseSlot = 2,
CreditUpdate = 3,
SlotHeartbeat = 4,
}
impl RecordType {
pub(crate) const fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(Self::Data),
1 => Some(Self::OpenSlot),
2 => Some(Self::CloseSlot),
3 => Some(Self::CreditUpdate),
4 => Some(Self::SlotHeartbeat),
_ => None,
}
}
pub(crate) const fn as_u8(self) -> u8 {
self as u8
}
#[cfg(test)]
pub(crate) const fn is_control(self) -> bool {
matches!(self, Self::OpenSlot | Self::CloseSlot | Self::CreditUpdate)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub(crate) enum CloseReason {
TerminalSent = 0,
PeerGone = 1,
UnknownSlot = 2,
ProtocolError = 3,
}
impl CloseReason {
pub(crate) const fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(Self::TerminalSent),
1 => Some(Self::PeerGone),
2 => Some(Self::UnknownSlot),
3 => Some(Self::ProtocolError),
_ => None,
}
}
pub(crate) const fn as_u8(self) -> u8 {
self as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RecordBody<'a> {
Data(&'a [u8]),
OpenSlot { anchor_id: u64, session_id: u64 },
CloseSlot { reason: CloseReason },
CreditUpdate { delta: u32 },
SlotHeartbeat,
}
impl RecordBody<'_> {
#[cfg(test)]
pub(crate) const fn record_type(&self) -> RecordType {
match self {
Self::Data(_) => RecordType::Data,
Self::OpenSlot { .. } => RecordType::OpenSlot,
Self::CloseSlot { .. } => RecordType::CloseSlot,
Self::CreditUpdate { .. } => RecordType::CreditUpdate,
Self::SlotHeartbeat => RecordType::SlotHeartbeat,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Record<'a> {
pub(crate) slot: SlotId,
pub(crate) frame_seq: u32,
pub(crate) body: RecordBody<'a>,
pub(crate) body_range: Range<usize>,
}
impl Record<'_> {
#[cfg(test)]
pub(crate) const fn record_type(&self) -> RecordType {
self.body.record_type()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub(crate) enum EncodeError {
#[error("batch is full at {MAX_RECORDS_PER_BATCH} records")]
BatchFull,
#[error("record body of {len} bytes exceeds the u32 length field")]
BodyTooLarge { len: usize },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub(crate) enum DecodeError {
#[error("batch payload of {len} bytes is shorter than the {BATCH_HEADER_LEN}-byte header")]
TruncatedBatchHeader { len: usize },
#[error("unsupported mux wire version {version}, expected {MUX_VERSION}")]
UnsupportedVersion { version: u8 },
#[error("batch declared {declared} records but ended after {decoded}")]
RecordCountMismatch { declared: u16, decoded: u16 },
#[error("record header at offset {offset} needs {RECORD_HEADER_LEN} bytes, {remaining} remain")]
TruncatedRecordHeader { offset: usize, remaining: usize },
#[error("record at offset {offset} declared a {declared_len}-byte body, {remaining} remain")]
TruncatedRecordBody {
offset: usize,
declared_len: u32,
remaining: usize,
},
#[error("unknown record type {value} at offset {offset}")]
UnknownRecordType { offset: usize, value: u8 },
#[error(
"record type {record_type:?} at offset {offset} requires a {expected}-byte body, got {actual}"
)]
BodyLengthMismatch {
offset: usize,
record_type: RecordType,
expected: usize,
actual: u32,
},
#[error("unknown close reason {value} at offset {offset}")]
UnknownCloseReason { offset: usize, value: u8 },
#[error("{remaining} trailing bytes after the declared records, at offset {offset}")]
TrailingBytes { offset: usize, remaining: usize },
}
pub(crate) const fn record_encoded_len(body_len: usize) -> Option<usize> {
body_len.checked_add(RECORD_HEADER_LEN)
}
pub(crate) struct BatchEncoder {
buf: BytesMut,
record_count: u16,
}
impl BatchEncoder {
pub(crate) fn new(peer_epoch: u64, batch_seq: u32) -> Self {
Self::with_buffer(BytesMut::new(), peer_epoch, batch_seq)
}
pub(crate) fn with_buffer(mut buf: BytesMut, peer_epoch: u64, batch_seq: u32) -> Self {
buf.clear();
BatchHeader::new(peer_epoch, batch_seq).encode_into(&mut buf);
Self {
buf,
record_count: 0,
}
}
pub(crate) const fn record_count(&self) -> u16 {
self.record_count
}
pub(crate) const fn is_empty(&self) -> bool {
self.record_count == 0
}
pub(crate) fn encoded_len(&self) -> usize {
self.buf.len()
}
pub(crate) fn push_data(
&mut self,
slot: SlotId,
frame_seq: u32,
body: &[u8],
) -> Result<(), EncodeError> {
self.push(RecordType::Data, slot, frame_seq, body.len(), |buf| {
buf.put_slice(body);
})
}
pub(crate) fn push_open_slot(
&mut self,
slot: SlotId,
frame_seq: u32,
anchor_id: u64,
session_id: u64,
) -> Result<(), EncodeError> {
self.push(RecordType::OpenSlot, slot, frame_seq, 16, |buf| {
buf.put_u64(anchor_id);
buf.put_u64(session_id);
})
}
pub(crate) fn push_close_slot(
&mut self,
slot: SlotId,
frame_seq: u32,
reason: CloseReason,
) -> Result<(), EncodeError> {
self.push(RecordType::CloseSlot, slot, frame_seq, 1, |buf| {
buf.put_u8(reason.as_u8());
})
}
pub(crate) fn push_credit_update(
&mut self,
slot: SlotId,
frame_seq: u32,
delta: u32,
) -> Result<(), EncodeError> {
self.push(RecordType::CreditUpdate, slot, frame_seq, 4, |buf| {
buf.put_u32(delta);
})
}
#[cfg(test)]
pub(crate) fn push_heartbeat(
&mut self,
slot: SlotId,
frame_seq: u32,
) -> Result<(), EncodeError> {
self.push(RecordType::SlotHeartbeat, slot, frame_seq, 0, |_| {})
}
pub(crate) fn finish(mut self) -> BytesMut {
if let Some(field) = self.buf.get_mut(2..4) {
field.copy_from_slice(&self.record_count.to_be_bytes());
}
self.buf
}
fn push<F>(
&mut self,
record_type: RecordType,
slot: SlotId,
frame_seq: u32,
body_len: usize,
write_body: F,
) -> Result<(), EncodeError>
where
F: FnOnce(&mut BytesMut),
{
if self.record_count == MAX_RECORDS_PER_BATCH {
return Err(EncodeError::BatchFull);
}
let len =
u32::try_from(body_len).map_err(|_| EncodeError::BodyTooLarge { len: body_len })?;
self.buf.put_u8(record_type.as_u8());
self.buf.put_u32(slot.raw());
self.buf.put_u32(frame_seq);
self.buf.put_u32(len);
write_body(&mut self.buf);
self.record_count += 1;
Ok(())
}
}
impl fmt::Debug for BatchEncoder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BatchEncoder")
.field("record_count", &self.record_count)
.field("encoded_len", &self.buf.len())
.finish()
}
}
#[derive(Debug)]
pub(crate) struct BatchDecoder<'a> {
payload: &'a [u8],
header: BatchHeader,
offset: usize,
decoded: u16,
done: bool,
}
impl<'a> BatchDecoder<'a> {
pub(crate) fn new(payload: &'a [u8]) -> Result<Self, DecodeError> {
let header = BatchHeader::decode(payload)?;
if !header.is_supported() {
return Err(DecodeError::UnsupportedVersion {
version: header.mux_version,
});
}
Ok(Self {
payload,
header,
offset: BATCH_HEADER_LEN,
decoded: 0,
done: false,
})
}
#[cfg(test)]
pub(crate) const fn header(&self) -> BatchHeader {
self.header
}
#[cfg(test)]
pub(crate) const fn decoded(&self) -> u16 {
self.decoded
}
fn next_record(&mut self) -> Result<Record<'a>, DecodeError> {
let offset = self.offset;
let remaining = self.payload.len().saturating_sub(offset);
let (Some(type_byte), Some(slot), Some(frame_seq), Some(len)) = (
read_u8(self.payload, offset),
read_u32(self.payload, offset + 1),
read_u32(self.payload, offset + 5),
read_u32(self.payload, offset + 9),
) else {
return Err(if remaining == 0 {
DecodeError::RecordCountMismatch {
declared: self.header.record_count,
decoded: self.decoded,
}
} else {
DecodeError::TruncatedRecordHeader { offset, remaining }
});
};
let record_type = RecordType::from_u8(type_byte).ok_or(DecodeError::UnknownRecordType {
offset,
value: type_byte,
})?;
let body_start = offset + RECORD_HEADER_LEN;
let body_len = usize::try_from(len).unwrap_or(usize::MAX);
let body = body_start
.checked_add(body_len)
.and_then(|end| self.payload.get(body_start..end))
.ok_or(DecodeError::TruncatedRecordBody {
offset,
declared_len: len,
remaining: self.payload.len().saturating_sub(body_start),
})?;
let decoded_body = decode_body(record_type, body, offset, len)?;
self.offset = body_start + body.len();
self.decoded += 1;
Ok(Record {
slot: SlotId::from_raw(slot),
frame_seq,
body: decoded_body,
body_range: body_start..body_start + body.len(),
})
}
}
fn decode_body(
record_type: RecordType,
body: &[u8],
offset: usize,
declared_len: u32,
) -> Result<RecordBody<'_>, DecodeError> {
let mismatch = |expected: usize| DecodeError::BodyLengthMismatch {
offset,
record_type,
expected,
actual: declared_len,
};
match record_type {
RecordType::Data => Ok(RecordBody::Data(body)),
RecordType::OpenSlot => {
let (Some(anchor_id), Some(session_id)) = (read_u64(body, 0), read_u64(body, 8)) else {
return Err(mismatch(16));
};
if body.len() != 16 {
return Err(mismatch(16));
}
Ok(RecordBody::OpenSlot {
anchor_id,
session_id,
})
}
RecordType::CloseSlot => {
let Some(byte) = read_u8(body, 0) else {
return Err(mismatch(1));
};
if body.len() != 1 {
return Err(mismatch(1));
}
let reason = CloseReason::from_u8(byte).ok_or(DecodeError::UnknownCloseReason {
offset,
value: byte,
})?;
Ok(RecordBody::CloseSlot { reason })
}
RecordType::CreditUpdate => {
let Some(delta) = read_u32(body, 0) else {
return Err(mismatch(4));
};
if body.len() != 4 {
return Err(mismatch(4));
}
Ok(RecordBody::CreditUpdate { delta })
}
RecordType::SlotHeartbeat => {
if !body.is_empty() {
return Err(mismatch(0));
}
Ok(RecordBody::SlotHeartbeat)
}
}
}
impl<'a> Iterator for BatchDecoder<'a> {
type Item = Result<Record<'a>, DecodeError>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
if self.decoded == self.header.record_count {
self.done = true;
let remaining = self.payload.len().saturating_sub(self.offset);
if remaining > 0 {
return Some(Err(DecodeError::TrailingBytes {
offset: self.offset,
remaining,
}));
}
return None;
}
match self.next_record() {
Ok(record) => Some(Ok(record)),
Err(err) => {
self.done = true;
Some(Err(err))
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
if self.done {
return (0, Some(0));
}
(
0,
Some(usize::from(self.header.record_count - self.decoded)),
)
}
}
impl FusedIterator for BatchDecoder<'_> {}
#[cfg(test)]
pub(crate) fn batch_seq_cmp(a: u32, b: u32) -> Ordering {
(a.wrapping_sub(b) as i32).cmp(&0)
}
#[cfg(test)]
pub(crate) fn batch_seq_is_newer(candidate: u32, last_seen: u32) -> bool {
batch_seq_cmp(candidate, last_seen) == Ordering::Greater
}
pub(crate) fn batch_seq_gap(expected: u32, received: u32) -> u32 {
received.wrapping_sub(expected)
}
#[cfg(test)]
mod tests;