1use crate::{LinkSessionId, NodeId, RequestId, layout::Fixed};
4
5pub const VERSION: u8 = 1;
7
8pub const ENVELOPE_BYTES: usize = 64;
10
11macro_rules! opcodes {
12 ($($variant:ident = $value:literal => $name:literal),+ $(,)?) => {
13 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
15 #[repr(u8)]
16 pub enum Opcode {
17 $(#[doc = $name] $variant = $value,)+
18 }
19
20 impl Opcode {
21 fn decode(value: u8) -> Result<Self, EnvelopeError> {
22 match value {
23 $($value => Ok(Self::$variant),)+
24 _ => Err(EnvelopeError::Opcode(value)),
25 }
26 }
27 }
28 };
29}
30
31opcodes! {
32 Hello = 0x01 => "`HELLO`: initiate link negotiation.",
33 Welcome = 0x02 => "`WELCOME`: establish the negotiated link session.",
34 Error = 0x03 => "`ERROR`: bounded protocol error.",
35 Append = 0x10 => "`APPEND`: submit producer records.",
36 Appended = 0x11 => "`APPENDED`: confirm the configured append boundary.",
37 LookupAppend = 0x12 => "`LOOKUP_APPEND`: resolve an uncertain append.",
38 AppendState = 0x13 => "`APPEND_STATE`: report an append outcome.",
39 OpenProducer = 0x14 => "`OPEN_PRODUCER`: open or fence a producer session.",
40 ProducerOpened = 0x15 => "`PRODUCER_OPENED`: confirm producer authority.",
41 AckResults = 0x16 => "`ACK_RESULTS`: advance producer retry-result floors.",
42 ResultsAcked = 0x17 => "`RESULTS_ACKED`: confirm result-floor commit.",
43 Subscribe = 0x20 => "`SUBSCRIBE`: request record delivery.",
44 Subscribed = 0x21 => "`SUBSCRIBED`: describe assigned delivery ranges.",
45 Records = 0x22 => "`RECORDS`: deliver committed records.",
46 Ack = 0x23 => "`ACK`: report consumer receipt or processing observations.",
47 ProgressCommit = 0x24 => "`PROGRESS_COMMIT`: request durable consumer progress.",
48 ProgressCommitted = 0x25 => "`PROGRESS_COMMITTED`: confirm committed progress.",
49 Unsubscribe = 0x27 => "`UNSUBSCRIBE`: cancel a subscription generation.",
50 Unsubscribed = 0x28 => "`UNSUBSCRIBED`: confirm subscription cancellation.",
51 ReplicaOpen = 0x30 => "`REPLICA_OPEN`: open a configured replica group.",
52 ReplicaState = 0x31 => "`REPLICA_STATE`: report replica state and volatile receipt.",
53 Prepare = 0x32 => "`PREPARE`: replicate canonical operations.",
54 PrepareOk = 0x33 => "`PREPARE_OK`: report contiguous replication evidence.",
55 Commit = 0x34 => "`COMMIT`: announce the quorum-committed prefix.",
56 StartViewChange = 0x35 => "`START_VIEW_CHANGE`: initiate a higher view.",
57 DoViewChange = 0x36 => "`DO_VIEW_CHANGE`: report immutable selected history.",
58 StartView = 0x37 => "`START_VIEW`: announce installed view and history.",
59 Recovery = 0x38 => "`RECOVERY`: request nonvoting recovery evidence.",
60 RecoveryState = 0x39 => "`RECOVERY_STATE`: reply to a fresh recovery nonce.",
61 FetchOps = 0x3a => "`FETCH_OPS`: request bounded canonical history.",
62 Ops = 0x3b => "`OPS`: transfer canonical operation bodies.",
63 SnapshotBegin = 0x3c => "`SNAPSHOT_BEGIN`: describe a checkpoint transfer.",
64 SnapshotChunk = 0x3d => "`SNAPSHOT_CHUNK`: transfer a bounded checkpoint chunk.",
65 SnapshotEnd = 0x3e => "`SNAPSHOT_END`: finish a checkpoint transfer.",
66 SnapshotInstalled = 0x3f => "`SNAPSHOT_INSTALLED`: confirm checkpoint installation.",
67 StateSnapshotRequest = 0x40 => "`STATE_SNAPSHOT_REQUEST`: request directory state.",
68 StateSnapshot = 0x41 => "`STATE_SNAPSHOT`: transfer directory state.",
69 StateUpdate = 0x42 => "`STATE_UPDATE`: advance directory state.",
70 StateResync = 0x43 => "`STATE_RESYNC`: request directory resynchronization.",
71 ExitView = 0x50 => "`EXIT_VIEW`: request quorum-supported departure from the current view.",
72 PrepareFlow = 0x51 => "`PREPARE_FLOW`: replicate canonical operations in a fenced receive epoch.",
73 PreparePub = 0x52 => "`PREPARE_PUB`: publish canonical operations to followers.",
74 RecordsPub = 0x53 => "`RECORDS_PUB`: publish confirmed records to every live reader.",
75 ReplicaReceipt = 0x54 => "`REPLICA_RECEIPT`: compact same-channel volatile receipt.",
76 HistoryRetired = 0x55 => "`HISTORY_RETIRED`: require nonvoting checkpoint recovery below retained history.",
77 GroupJoin = 0x60 => "`GROUP_JOIN`: join, heartbeat, or leave a consumer group on its coordinator.",
78 GroupJoined = 0x61 => "`GROUP_JOINED`: describe the committed membership view of one instance.",
79 GroupAssign = 0x62 => "`GROUP_ASSIGN`: relay a committed plan revision to a source partition.",
80 GroupAssigned = 0x63 => "`GROUP_ASSIGNED`: confirm source activation coordinates.",
81 GroupInspect = 0x64 => "`GROUP_INSPECT`: read a group's committed coordinator image.",
82 GroupInspected = 0x65 => "`GROUP_INSPECTED`: describe a group's committed coordinator image.",
83 GroupDelete = 0x66 => "`GROUP_DELETE`: retire one group incarnation on its coordinator.",
84 GroupDeleted = 0x67 => "`GROUP_DELETED`: confirm a group incarnation is retired.",
85 Nack = 0x7f => "`NACK`: report a typed negative outcome.",
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct EnvelopeLimits {
91 pub max_metadata_bytes: usize,
93 pub max_payload_bytes: usize,
95}
96
97impl Default for EnvelopeLimits {
98 fn default() -> Self {
99 Self {
100 max_metadata_bytes: 64 * 1024,
101 max_payload_bytes: 1024 * 1024,
102 }
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct Envelope {
109 pub opcode: Opcode,
111 pub response: bool,
113 pub request_id: Option<RequestId>,
115 pub sender: NodeId,
117 pub session: Option<LinkSessionId>,
120}
121
122impl Envelope {
123 pub fn validate_frames(
126 self,
127 metadata_bytes: usize,
128 payload_bytes: usize,
129 limits: EnvelopeLimits,
130 ) -> Result<(), EnvelopeError> {
131 validate_lengths(metadata_bytes, payload_bytes, limits)?;
132 self.validate(payload_bytes)?;
133 u32::try_from(metadata_bytes).map_err(|_| EnvelopeError::Length)?;
134 u32::try_from(payload_bytes).map_err(|_| EnvelopeError::Length)?;
135 Ok(())
136 }
137
138 pub fn encode_header(
143 self,
144 metadata_bytes: usize,
145 payload_bytes: usize,
146 limits: EnvelopeLimits,
147 ) -> Result<[u8; ENVELOPE_BYTES], EnvelopeError> {
148 self.validate_frames(metadata_bytes, payload_bytes, limits)?;
149 let mut bytes = [0; ENVELOPE_BYTES];
150 bytes[..4].copy_from_slice(b"OZY\0");
151 bytes[4] = VERSION;
152 bytes[5] = self.opcode as u8;
153 bytes[6..8].copy_from_slice(&u16::from(self.response).to_be_bytes());
154 if let Some(request) = self.request_id {
155 bytes[8..24].copy_from_slice(request.as_bytes());
156 }
157 bytes[24..40].copy_from_slice(self.sender.as_bytes());
158 if let Some(session) = self.session {
159 bytes[40..56].copy_from_slice(session.as_bytes());
160 }
161 bytes[56..60].copy_from_slice(&(metadata_bytes as u32).to_be_bytes());
162 bytes[60..64].copy_from_slice(&(payload_bytes as u32).to_be_bytes());
163 Ok(bytes)
164 }
165
166 fn validate(self, payload_bytes: usize) -> Result<(), EnvelopeError> {
167 if self.sender.as_bytes() == &[0; 16]
168 || self.request_id.is_some_and(|id| id.as_bytes() == &[0; 16])
169 || self.session.is_some_and(|id| id.as_bytes() == &[0; 16])
170 {
171 return Err(EnvelopeError::ZeroIdentity);
172 }
173 if self.response && self.request_id.is_none() {
174 return Err(EnvelopeError::Correlation);
175 }
176 if self.opcode == Opcode::Hello {
177 if self.session.is_some()
178 || self.response
179 || self.request_id.is_none()
180 || payload_bytes != 0
181 {
182 return Err(EnvelopeError::Handshake);
183 }
184 } else if matches!(self.opcode, Opcode::PreparePub | Opcode::RecordsPub) {
185 if self.session.is_some() || self.response || self.request_id.is_some() {
186 return Err(EnvelopeError::Session);
187 }
188 } else if self.session.is_none() {
189 return Err(EnvelopeError::Session);
190 } else if self.opcode == Opcode::Welcome && (!self.response || self.request_id.is_none()) {
191 return Err(EnvelopeError::Handshake);
192 }
193 Ok(())
194 }
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub struct Packet<'a> {
203 pub envelope: Envelope,
205 pub metadata: &'a [u8],
207 pub payload: &'a [u8],
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub struct Frames<T> {
215 pub metadata: T,
217 pub payload: T,
219}
220
221pub fn decode_packet<'a>(
226 frames: &[&'a [u8]],
227 limits: EnvelopeLimits,
228) -> Result<Packet<'a>, EnvelopeError> {
229 let [header, metadata, payload] = frames else {
230 return Err(EnvelopeError::FrameCount);
231 };
232 let header: &[u8; ENVELOPE_BYTES] = (*header)
233 .try_into()
234 .map_err(|_| EnvelopeError::HeaderLength)?;
235 validate_lengths(metadata.len(), payload.len(), limits)?;
236 if header.array::<0, 4>() != *b"OZY\0" {
237 return Err(EnvelopeError::Magic);
238 }
239 if header.u8_at::<4>() != VERSION {
240 return Err(EnvelopeError::Major(header.u8_at::<4>()));
241 }
242 let opcode = Opcode::decode(header.u8_at::<5>())?;
243 let flags = header.u16_at::<6>();
244 if flags & !1 != 0 {
245 return Err(EnvelopeError::Flags);
246 }
247 let metadata_bytes = header.u32_at::<56>();
248 let payload_bytes = header.u32_at::<60>();
249 if usize::try_from(metadata_bytes) != Ok(metadata.len())
250 || usize::try_from(payload_bytes) != Ok(payload.len())
251 {
252 return Err(EnvelopeError::Length);
253 }
254 let request = header.array::<8, 16>();
255 let session = header.array::<40, 16>();
256 let envelope = Envelope {
257 opcode,
258 response: flags == 1,
259 request_id: (request != [0; 16]).then(|| RequestId::from_bytes(request)),
260 sender: NodeId::from_bytes(header.array::<24, 16>()),
261 session: (session != [0; 16]).then(|| LinkSessionId::from_bytes(session)),
262 };
263 envelope.validate(payload.len())?;
264 Ok(Packet {
265 envelope,
266 metadata,
267 payload,
268 })
269}
270
271fn validate_lengths(
272 metadata: usize,
273 payload: usize,
274 limits: EnvelopeLimits,
275) -> Result<(), EnvelopeError> {
276 if metadata > limits.max_metadata_bytes || payload > limits.max_payload_bytes {
277 return Err(EnvelopeError::Limit);
278 }
279 Ok(())
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
284pub enum EnvelopeError {
285 #[error("native packet requires exactly three Ozzy frames")]
287 FrameCount,
288 #[error("invalid native envelope length")]
290 HeaderLength,
291 #[error("invalid Ozzy magic")]
293 Magic,
294 #[error("unsupported Ozzy major {0}")]
296 Major(u8),
297 #[error("unknown Ozzy opcode {0}")]
299 Opcode(u8),
300 #[error("unsupported Ozzy envelope flags")]
302 Flags,
303 #[error("Ozzy frame length mismatch or overflow")]
305 Length,
306 #[error("Ozzy frame exceeds receive limit")]
308 Limit,
309 #[error("zero Ozzy envelope identity")]
311 ZeroIdentity,
312 #[error("response requires a nonzero request ID")]
314 Correlation,
315 #[error("command requires a negotiated link session")]
317 Session,
318 #[error("invalid handshake envelope")]
320 Handshake,
321}