1use super::causal::CausalContext;
2use super::error::ProtocolError;
3
4const U32_LEN: usize = 4;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
8pub struct SchemaId([u8; Self::WIRE_LEN]);
9
10impl SchemaId {
11 pub const WIRE_LEN: usize = 32;
13
14 #[must_use]
16 pub const fn new(bytes: [u8; Self::WIRE_LEN]) -> Self {
17 Self(bytes)
18 }
19
20 #[must_use]
22 pub const fn as_bytes(&self) -> &[u8; Self::WIRE_LEN] {
23 &self.0
24 }
25
26 #[must_use]
28 pub const fn into_bytes(self) -> [u8; Self::WIRE_LEN] {
29 self.0
30 }
31
32 #[must_use]
49 pub fn from_schema_bytes(schema_bytes: &[u8]) -> Self {
50 let mut id = [0_u8; Self::WIRE_LEN];
51 let mut hash = fnv1a(schema_bytes).to_be_bytes();
52 for (index, slot) in id.iter_mut().enumerate() {
53 *slot = hash[index % hash.len()];
54 if index % hash.len() == hash.len() - 1 {
55 hash = fnv1a(&hash).to_be_bytes();
56 }
57 }
58 Self(id)
59 }
60}
61
62fn fnv1a(bytes: &[u8]) -> u64 {
64 const OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
65 const PRIME: u64 = 0x0000_0100_0000_01b3;
66 let mut hash = OFFSET_BASIS;
67 for byte in bytes {
68 hash ^= u64::from(*byte);
69 hash = hash.wrapping_mul(PRIME);
70 }
71 hash
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
76pub struct MessageEnvelope {
77 pub schema_id: SchemaId,
79 pub causal_context: CausalContext,
81 pub payload: Vec<u8>,
83}
84
85impl MessageEnvelope {
86 #[must_use]
88 pub const fn new(schema_id: SchemaId, causal_context: CausalContext, payload: Vec<u8>) -> Self {
89 Self {
90 schema_id,
91 causal_context,
92 payload,
93 }
94 }
95
96 pub fn encoded_len(&self) -> Result<usize, ProtocolError> {
104 let causal_len = self.causal_context.encoded_len()?;
105 checked_u32_len(causal_len, "causal context")?;
106 checked_u32_len(self.payload.len(), "payload")?;
107 sum_lengths(&[
108 SchemaId::WIRE_LEN,
109 U32_LEN,
110 causal_len,
111 U32_LEN,
112 self.payload.len(),
113 ])
114 }
115
116 pub fn serialize(&self) -> Result<Vec<u8>, ProtocolError> {
123 let causal_bytes = self.causal_context.serialize()?;
124 checked_u32_len(causal_bytes.len(), "causal context")?;
125 checked_u32_len(self.payload.len(), "payload")?;
126 let len = sum_lengths(&[
127 SchemaId::WIRE_LEN,
128 U32_LEN,
129 causal_bytes.len(),
130 U32_LEN,
131 self.payload.len(),
132 ])?;
133 let mut bytes = Vec::with_capacity(len);
134
135 bytes.extend_from_slice(self.schema_id.as_bytes());
136 write_u32(&mut bytes, causal_bytes.len(), "causal context")?;
137 bytes.extend_from_slice(&causal_bytes);
138 write_u32(&mut bytes, self.payload.len(), "payload")?;
139 bytes.extend_from_slice(&self.payload);
140
141 if bytes.len() == len {
142 Ok(bytes)
143 } else {
144 Err(ProtocolError::codec(
145 "message envelope encoder produced an unexpected length",
146 ))
147 }
148 }
149
150 pub fn to_wire_bytes(&self) -> Result<Vec<u8>, ProtocolError> {
157 self.serialize()
158 }
159
160 pub fn deserialize(bytes: &[u8]) -> Result<Self, ProtocolError> {
167 Self::from_wire_bytes(bytes)
168 }
169
170 pub fn from_wire_bytes(bytes: &[u8]) -> Result<Self, ProtocolError> {
177 let mut offset = 0;
178 let schema_id = SchemaId::new(read_schema_id(bytes, &mut offset)?);
179 let causal_len = read_u32_as_usize(bytes, &mut offset, "causal context length")?;
180 let causal_bytes = read_slice(bytes, &mut offset, causal_len, "causal context bytes")?;
181 let causal_context = CausalContext::deserialize(causal_bytes)?;
182 let payload_len = read_u32_as_usize(bytes, &mut offset, "payload length")?;
183 let payload = read_slice(bytes, &mut offset, payload_len, "payload bytes")?.to_vec();
184
185 if offset == bytes.len() {
186 Ok(Self {
187 schema_id,
188 causal_context,
189 payload,
190 })
191 } else {
192 Err(ProtocolError::codec(
193 "message envelope contained trailing bytes",
194 ))
195 }
196 }
197}
198
199fn checked_u32_len(len: usize, field: &str) -> Result<(), ProtocolError> {
200 u32::try_from(len)
201 .map(|_| ())
202 .map_err(|_| ProtocolError::codec(format!("{field} length exceeded u32::MAX")))
203}
204
205fn sum_lengths(parts: &[usize]) -> Result<usize, ProtocolError> {
206 let mut total = 0_usize;
207 for part in parts {
208 total = total
209 .checked_add(*part)
210 .ok_or_else(|| ProtocolError::codec("message envelope length overflowed usize"))?;
211 }
212 Ok(total)
213}
214
215fn write_u32(buffer: &mut Vec<u8>, value: usize, field: &str) -> Result<(), ProtocolError> {
216 let value = u32::try_from(value)
217 .map_err(|_| ProtocolError::codec(format!("{field} length exceeded u32::MAX")))?;
218 buffer.extend_from_slice(&value.to_be_bytes());
219 Ok(())
220}
221
222fn read_schema_id(
223 bytes: &[u8],
224 offset: &mut usize,
225) -> Result<[u8; SchemaId::WIRE_LEN], ProtocolError> {
226 let schema_bytes = read_slice(bytes, offset, SchemaId::WIRE_LEN, "schema id")?;
227 let mut schema_id = [0_u8; SchemaId::WIRE_LEN];
228 schema_id.copy_from_slice(schema_bytes);
229 Ok(schema_id)
230}
231
232fn read_u32_as_usize(
233 bytes: &[u8],
234 offset: &mut usize,
235 field: &str,
236) -> Result<usize, ProtocolError> {
237 let bytes = read_slice(bytes, offset, U32_LEN, field)?;
238 let [b0, b1, b2, b3] = bytes else {
239 return Err(ProtocolError::codec(format!("{field} was truncated")));
240 };
241 usize::try_from(u32::from_be_bytes([*b0, *b1, *b2, *b3]))
242 .map_err(|_| ProtocolError::codec(format!("{field} cannot fit usize")))
243}
244
245fn read_slice<'a>(
246 bytes: &'a [u8],
247 offset: &mut usize,
248 len: usize,
249 field: &str,
250) -> Result<&'a [u8], ProtocolError> {
251 let end = offset
252 .checked_add(len)
253 .ok_or_else(|| ProtocolError::codec(format!("{field} offset overflowed usize")))?;
254 let Some(slice) = bytes.get(*offset..end) else {
255 return Err(ProtocolError::codec(format!(
256 "{field} exceeded available bytes"
257 )));
258 };
259 *offset = end;
260 Ok(slice)
261}
262
263#[cfg(test)]
264mod tests {
265 use std::fmt::Debug;
266
267 use super::{MessageEnvelope, SchemaId};
268 use crate::protocol::{CausalContext, MessageId, ProtocolError, extract_causal_context};
269
270 #[test]
271 fn envelope_trait_bounds_are_available() {
272 fn assert_schema_traits<T: Debug + Clone + Copy + PartialEq + Eq>() {}
273 fn assert_envelope_traits<T: Debug + Clone + PartialEq + Eq>() {}
274
275 assert_schema_traits::<SchemaId>();
276 assert_envelope_traits::<MessageEnvelope>();
277 }
278
279 #[test]
280 fn schema_id_wraps_exactly_thirty_two_bytes() {
281 let bytes = [0xAB; SchemaId::WIRE_LEN];
282 let schema_id = SchemaId::new(bytes);
283
284 assert_eq!(SchemaId::WIRE_LEN, 32);
285 assert_eq!(schema_id.as_bytes(), &bytes);
286 assert_eq!(schema_id.into_bytes(), bytes);
287 }
288
289 #[test]
290 fn constructor_sets_all_fields() {
291 let schema_id = SchemaId::new([1; 32]);
292 let causal_context = CausalContext::with_parent(MessageId::from("parent"));
293 let payload = vec![1, 2, 3];
294
295 let envelope = MessageEnvelope::new(schema_id, causal_context.clone(), payload.clone());
296
297 assert_eq!(envelope.schema_id, schema_id);
298 assert_eq!(envelope.causal_context, causal_context);
299 assert_eq!(envelope.payload, payload);
300 }
301
302 #[test]
303 fn identical_fields_produce_identical_bytes() -> Result<(), ProtocolError> {
304 let first = sample_envelope(vec![5, 6, 7]);
305 let second = sample_envelope(vec![5, 6, 7]);
306
307 assert_eq!(first.serialize()?, second.serialize()?);
308 Ok(())
309 }
310
311 #[test]
312 fn serialization_round_trips_losslessly() -> Result<(), ProtocolError> {
313 let envelope = sample_envelope(vec![9, 8, 7]);
314 let encoded = envelope.serialize()?;
315 let decoded = MessageEnvelope::deserialize(&encoded)?;
316
317 assert_eq!(decoded, envelope);
318 Ok(())
319 }
320
321 #[test]
322 fn encoded_layout_starts_with_schema_id_and_big_endian_lengths() -> Result<(), ProtocolError> {
323 let schema_id = SchemaId::new([0x42; 32]);
324 let causal_context = CausalContext {
325 parent_id: Some(MessageId::from("parent")),
326 vector_clock_entry: Some(0x0102_0304_0506_0708),
327 };
328 let causal_len = causal_context.encoded_len()?;
329 let envelope = MessageEnvelope::new(schema_id, causal_context, vec![0xAA, 0xBB]);
330 let encoded = envelope.serialize()?;
331
332 assert_eq!(&encoded[..32], schema_id.as_bytes());
333 assert_eq!(
334 &encoded[32..36],
335 &u32::try_from(causal_len)
336 .map_err(|_| ProtocolError::codec("test causal length exceeded u32"))?
337 .to_be_bytes()
338 );
339 let payload_len_offset = 36 + causal_len;
340 assert_eq!(
341 &encoded[payload_len_offset..payload_len_offset + 4],
342 &2_u32.to_be_bytes()
343 );
344 Ok(())
345 }
346
347 #[test]
348 fn empty_payload_round_trips() -> Result<(), ProtocolError> {
349 let envelope = sample_envelope(Vec::new());
350 let encoded = envelope.serialize()?;
351 let decoded = MessageEnvelope::deserialize(&encoded)?;
352
353 assert_eq!(decoded, envelope);
354 assert_eq!(decoded.payload, Vec::<u8>::new());
355 Ok(())
356 }
357
358 #[test]
359 fn causal_context_is_extractable_from_envelope_bytes() -> Result<(), ProtocolError> {
360 let envelope = sample_envelope(vec![1, 2, 3, 4]);
361 let encoded = envelope.serialize()?;
362
363 assert_eq!(extract_causal_context(&encoded)?, envelope.causal_context);
364 Ok(())
365 }
366
367 fn sample_envelope(payload: Vec<u8>) -> MessageEnvelope {
368 MessageEnvelope::new(
369 SchemaId::new([0x11; 32]),
370 CausalContext {
371 parent_id: Some(MessageId::from("parent-1")),
372 vector_clock_entry: Some(99),
373 },
374 payload,
375 )
376 }
377}