use crate::{LinkSessionId, NodeId, RequestId, layout::Fixed};
pub const VERSION: u8 = 1;
pub const ENVELOPE_BYTES: usize = 64;
macro_rules! opcodes {
($($variant:ident = $value:literal => $name:literal),+ $(,)?) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Opcode {
$(#[doc = $name] $variant = $value,)+
}
impl Opcode {
fn decode(value: u8) -> Result<Self, EnvelopeError> {
match value {
$($value => Ok(Self::$variant),)+
_ => Err(EnvelopeError::Opcode(value)),
}
}
}
};
}
opcodes! {
Hello = 0x01 => "`HELLO`: initiate link negotiation.",
Welcome = 0x02 => "`WELCOME`: establish the negotiated link session.",
Error = 0x03 => "`ERROR`: bounded protocol error.",
Append = 0x10 => "`APPEND`: submit producer records.",
Appended = 0x11 => "`APPENDED`: confirm the configured append boundary.",
LookupAppend = 0x12 => "`LOOKUP_APPEND`: resolve an uncertain append.",
AppendState = 0x13 => "`APPEND_STATE`: report an append outcome.",
OpenProducer = 0x14 => "`OPEN_PRODUCER`: open or fence a producer session.",
ProducerOpened = 0x15 => "`PRODUCER_OPENED`: confirm producer authority.",
AckResults = 0x16 => "`ACK_RESULTS`: advance producer retry-result floors.",
ResultsAcked = 0x17 => "`RESULTS_ACKED`: confirm result-floor commit.",
Subscribe = 0x20 => "`SUBSCRIBE`: request record delivery.",
Subscribed = 0x21 => "`SUBSCRIBED`: describe assigned delivery ranges.",
Records = 0x22 => "`RECORDS`: deliver committed records.",
Ack = 0x23 => "`ACK`: report consumer receipt or processing observations.",
ProgressCommit = 0x24 => "`PROGRESS_COMMIT`: request durable consumer progress.",
ProgressCommitted = 0x25 => "`PROGRESS_COMMITTED`: confirm committed progress.",
Unsubscribe = 0x27 => "`UNSUBSCRIBE`: cancel a subscription generation.",
Unsubscribed = 0x28 => "`UNSUBSCRIBED`: confirm subscription cancellation.",
ReplicaOpen = 0x30 => "`REPLICA_OPEN`: open a configured replica group.",
ReplicaState = 0x31 => "`REPLICA_STATE`: report replica state and volatile receipt.",
Prepare = 0x32 => "`PREPARE`: replicate canonical operations.",
PrepareOk = 0x33 => "`PREPARE_OK`: report contiguous replication evidence.",
Commit = 0x34 => "`COMMIT`: announce the quorum-committed prefix.",
StartViewChange = 0x35 => "`START_VIEW_CHANGE`: initiate a higher view.",
DoViewChange = 0x36 => "`DO_VIEW_CHANGE`: report immutable selected history.",
StartView = 0x37 => "`START_VIEW`: announce installed view and history.",
Recovery = 0x38 => "`RECOVERY`: request nonvoting recovery evidence.",
RecoveryState = 0x39 => "`RECOVERY_STATE`: reply to a fresh recovery nonce.",
FetchOps = 0x3a => "`FETCH_OPS`: request bounded canonical history.",
Ops = 0x3b => "`OPS`: transfer canonical operation bodies.",
SnapshotBegin = 0x3c => "`SNAPSHOT_BEGIN`: describe a checkpoint transfer.",
SnapshotChunk = 0x3d => "`SNAPSHOT_CHUNK`: transfer a bounded checkpoint chunk.",
SnapshotEnd = 0x3e => "`SNAPSHOT_END`: finish a checkpoint transfer.",
SnapshotInstalled = 0x3f => "`SNAPSHOT_INSTALLED`: confirm checkpoint installation.",
StateSnapshotRequest = 0x40 => "`STATE_SNAPSHOT_REQUEST`: request directory state.",
StateSnapshot = 0x41 => "`STATE_SNAPSHOT`: transfer directory state.",
StateUpdate = 0x42 => "`STATE_UPDATE`: advance directory state.",
StateResync = 0x43 => "`STATE_RESYNC`: request directory resynchronization.",
ExitView = 0x50 => "`EXIT_VIEW`: request quorum-supported departure from the current view.",
PrepareFlow = 0x51 => "`PREPARE_FLOW`: replicate canonical operations in a fenced receive epoch.",
PreparePub = 0x52 => "`PREPARE_PUB`: publish canonical operations to followers.",
RecordsPub = 0x53 => "`RECORDS_PUB`: publish confirmed records to every live reader.",
ReplicaReceipt = 0x54 => "`REPLICA_RECEIPT`: compact same-channel volatile receipt.",
HistoryRetired = 0x55 => "`HISTORY_RETIRED`: require nonvoting checkpoint recovery below retained history.",
GroupJoin = 0x60 => "`GROUP_JOIN`: join, heartbeat, or leave a consumer group on its coordinator.",
GroupJoined = 0x61 => "`GROUP_JOINED`: describe the committed membership view of one instance.",
GroupAssign = 0x62 => "`GROUP_ASSIGN`: relay a committed plan revision to a source partition.",
GroupAssigned = 0x63 => "`GROUP_ASSIGNED`: confirm source activation coordinates.",
GroupInspect = 0x64 => "`GROUP_INSPECT`: read a group's committed coordinator image.",
GroupInspected = 0x65 => "`GROUP_INSPECTED`: describe a group's committed coordinator image.",
GroupDelete = 0x66 => "`GROUP_DELETE`: retire one group incarnation on its coordinator.",
GroupDeleted = 0x67 => "`GROUP_DELETED`: confirm a group incarnation is retired.",
Nack = 0x7f => "`NACK`: report a typed negative outcome.",
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EnvelopeLimits {
pub max_metadata_bytes: usize,
pub max_payload_bytes: usize,
}
impl Default for EnvelopeLimits {
fn default() -> Self {
Self {
max_metadata_bytes: 64 * 1024,
max_payload_bytes: 1024 * 1024,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Envelope {
pub opcode: Opcode,
pub response: bool,
pub request_id: Option<RequestId>,
pub sender: NodeId,
pub session: Option<LinkSessionId>,
}
impl Envelope {
pub fn validate_frames(
self,
metadata_bytes: usize,
payload_bytes: usize,
limits: EnvelopeLimits,
) -> Result<(), EnvelopeError> {
validate_lengths(metadata_bytes, payload_bytes, limits)?;
self.validate(payload_bytes)?;
u32::try_from(metadata_bytes).map_err(|_| EnvelopeError::Length)?;
u32::try_from(payload_bytes).map_err(|_| EnvelopeError::Length)?;
Ok(())
}
pub fn encode_header(
self,
metadata_bytes: usize,
payload_bytes: usize,
limits: EnvelopeLimits,
) -> Result<[u8; ENVELOPE_BYTES], EnvelopeError> {
self.validate_frames(metadata_bytes, payload_bytes, limits)?;
let mut bytes = [0; ENVELOPE_BYTES];
bytes[..4].copy_from_slice(b"OZY\0");
bytes[4] = VERSION;
bytes[5] = self.opcode as u8;
bytes[6..8].copy_from_slice(&u16::from(self.response).to_be_bytes());
if let Some(request) = self.request_id {
bytes[8..24].copy_from_slice(request.as_bytes());
}
bytes[24..40].copy_from_slice(self.sender.as_bytes());
if let Some(session) = self.session {
bytes[40..56].copy_from_slice(session.as_bytes());
}
bytes[56..60].copy_from_slice(&(metadata_bytes as u32).to_be_bytes());
bytes[60..64].copy_from_slice(&(payload_bytes as u32).to_be_bytes());
Ok(bytes)
}
fn validate(self, payload_bytes: usize) -> Result<(), EnvelopeError> {
if self.sender.as_bytes() == &[0; 16]
|| self.request_id.is_some_and(|id| id.as_bytes() == &[0; 16])
|| self.session.is_some_and(|id| id.as_bytes() == &[0; 16])
{
return Err(EnvelopeError::ZeroIdentity);
}
if self.response && self.request_id.is_none() {
return Err(EnvelopeError::Correlation);
}
if self.opcode == Opcode::Hello {
if self.session.is_some()
|| self.response
|| self.request_id.is_none()
|| payload_bytes != 0
{
return Err(EnvelopeError::Handshake);
}
} else if matches!(self.opcode, Opcode::PreparePub | Opcode::RecordsPub) {
if self.session.is_some() || self.response || self.request_id.is_some() {
return Err(EnvelopeError::Session);
}
} else if self.session.is_none() {
return Err(EnvelopeError::Session);
} else if self.opcode == Opcode::Welcome && (!self.response || self.request_id.is_none()) {
return Err(EnvelopeError::Handshake);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Packet<'a> {
pub envelope: Envelope,
pub metadata: &'a [u8],
pub payload: &'a [u8],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Frames<T> {
pub metadata: T,
pub payload: T,
}
pub fn decode_packet<'a>(
frames: &[&'a [u8]],
limits: EnvelopeLimits,
) -> Result<Packet<'a>, EnvelopeError> {
let [header, metadata, payload] = frames else {
return Err(EnvelopeError::FrameCount);
};
let header: &[u8; ENVELOPE_BYTES] = (*header)
.try_into()
.map_err(|_| EnvelopeError::HeaderLength)?;
validate_lengths(metadata.len(), payload.len(), limits)?;
if header.array::<0, 4>() != *b"OZY\0" {
return Err(EnvelopeError::Magic);
}
if header.u8_at::<4>() != VERSION {
return Err(EnvelopeError::Major(header.u8_at::<4>()));
}
let opcode = Opcode::decode(header.u8_at::<5>())?;
let flags = header.u16_at::<6>();
if flags & !1 != 0 {
return Err(EnvelopeError::Flags);
}
let metadata_bytes = header.u32_at::<56>();
let payload_bytes = header.u32_at::<60>();
if usize::try_from(metadata_bytes) != Ok(metadata.len())
|| usize::try_from(payload_bytes) != Ok(payload.len())
{
return Err(EnvelopeError::Length);
}
let request = header.array::<8, 16>();
let session = header.array::<40, 16>();
let envelope = Envelope {
opcode,
response: flags == 1,
request_id: (request != [0; 16]).then(|| RequestId::from_bytes(request)),
sender: NodeId::from_bytes(header.array::<24, 16>()),
session: (session != [0; 16]).then(|| LinkSessionId::from_bytes(session)),
};
envelope.validate(payload.len())?;
Ok(Packet {
envelope,
metadata,
payload,
})
}
fn validate_lengths(
metadata: usize,
payload: usize,
limits: EnvelopeLimits,
) -> Result<(), EnvelopeError> {
if metadata > limits.max_metadata_bytes || payload > limits.max_payload_bytes {
return Err(EnvelopeError::Limit);
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum EnvelopeError {
#[error("native packet requires exactly three Ozzy frames")]
FrameCount,
#[error("invalid native envelope length")]
HeaderLength,
#[error("invalid Ozzy magic")]
Magic,
#[error("unsupported Ozzy major {0}")]
Major(u8),
#[error("unknown Ozzy opcode {0}")]
Opcode(u8),
#[error("unsupported Ozzy envelope flags")]
Flags,
#[error("Ozzy frame length mismatch or overflow")]
Length,
#[error("Ozzy frame exceeds receive limit")]
Limit,
#[error("zero Ozzy envelope identity")]
ZeroIdentity,
#[error("response requires a nonzero request ID")]
Correlation,
#[error("command requires a negotiated link session")]
Session,
#[error("invalid handshake envelope")]
Handshake,
}