1use dcbor::prelude::*;
7
8use crate::ProtocolError;
9
10const KEY_REQUEST_ID: u64 = 0;
11const KEY_NAMED_PATH: u64 = 1;
12const KEY_OBJECT_BYTES: u64 = 2;
13const KEY_ATTACHMENT_ID: u64 = 0;
14const KEY_CHUNK_INDEX: u64 = 1;
15const KEY_TOTAL_CHUNKS: u64 = 2;
16const KEY_DATA: u64 = 3;
17const KEY_REASON: u64 = 1;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[repr(u8)]
22pub enum FrameType {
23 RepairRequest = 0x01,
24 RepairResponse = 0x02,
25 RepairNotFound = 0x03,
26 SyncManifest = 0x10,
27 SyncChunk = 0x11,
28 SyncAck = 0x12,
29 AttachmentChunk = 0x20,
30 AttachmentChunkAck = 0x21,
31 AttachmentAbort = 0x22,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct PeerRepairRequest {
36 pub request_id: [u8; 16],
37 pub named_path: String,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct PeerRepairResponse {
42 pub request_id: [u8; 16],
43 pub object_bytes: Vec<u8>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct PeerRepairNotFound {
48 pub request_id: [u8; 16],
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct AttachmentChunk {
53 pub attachment_id: [u8; 16],
54 pub chunk_index: u32,
55 pub total_chunks: u32,
56 pub data: Vec<u8>,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct AttachmentChunkAck {
61 pub attachment_id: [u8; 16],
62 pub chunk_index: u32,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct AttachmentAbort {
67 pub attachment_id: [u8; 16],
68 pub reason: String,
69}
70
71pub fn frame_type(bytes: &[u8]) -> Option<FrameType> {
73 let first = *bytes.first()?;
74 match first {
75 0x01 => Some(FrameType::RepairRequest),
76 0x02 => Some(FrameType::RepairResponse),
77 0x03 => Some(FrameType::RepairNotFound),
78 0x10 => Some(FrameType::SyncManifest),
79 0x11 => Some(FrameType::SyncChunk),
80 0x12 => Some(FrameType::SyncAck),
81 0x20 => Some(FrameType::AttachmentChunk),
82 0x21 => Some(FrameType::AttachmentChunkAck),
83 0x22 => Some(FrameType::AttachmentAbort),
84 _ => None,
85 }
86}
87
88fn prepend_frame(frame: FrameType, body: Vec<u8>) -> Vec<u8> {
89 let mut out = Vec::with_capacity(1 + body.len());
90 out.push(frame as u8);
91 out.extend_from_slice(&body);
92 out
93}
94
95fn strip_frame(expected: FrameType, bytes: &[u8]) -> Result<&[u8], ProtocolError> {
96 match bytes.first() {
97 None => Err(ProtocolError::InvalidEncoding("empty frame".to_string())),
98 Some(&b) if b != expected as u8 => Err(ProtocolError::InvalidEncoding(format!(
99 "expected frame type 0x{:02x}, got 0x{:02x}",
100 expected as u8, b
101 ))),
102 _ => Ok(&bytes[1..]),
103 }
104}
105
106fn parse_map(bytes: &[u8]) -> Result<Map, ProtocolError> {
107 let cbor =
108 CBOR::try_from_data(bytes).map_err(|e| ProtocolError::InvalidEncoding(e.to_string()))?;
109 cbor.try_into_map()
110 .map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))
111}
112
113fn extract_fixed<const N: usize>(
114 map: &Map,
115 key: u64,
116 field: &str,
117) -> Result<[u8; N], ProtocolError> {
118 let cbor: CBOR = map
119 .extract(key)
120 .map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))?;
121 let bytes = cbor
122 .try_into_byte_string()
123 .map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))?;
124 bytes
125 .try_into()
126 .map_err(|_| ProtocolError::InvalidEnvelope(format!("{field} must be {N} bytes")))
127}
128
129fn extract_bytes(map: &Map, key: u64) -> Result<Vec<u8>, ProtocolError> {
130 let cbor: CBOR = map
131 .extract(key)
132 .map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))?;
133 cbor.try_into_byte_string()
134 .map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))
135}
136
137fn extract_u64(map: &Map, key: u64) -> Result<u64, ProtocolError> {
138 map.extract(key)
139 .map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))
140}
141
142fn extract_u32(map: &Map, key: u64, field: &str) -> Result<u32, ProtocolError> {
143 let value = extract_u64(map, key)?;
144 u32::try_from(value)
145 .map_err(|_| ProtocolError::InvalidEnvelope(format!("{field} exceeds u32 range: {value}")))
146}
147
148fn extract_text(map: &Map, key: u64) -> Result<String, ProtocolError> {
149 let cbor: CBOR = map
150 .extract(key)
151 .map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))?;
152 cbor.try_into_text()
153 .map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))
154}
155
156pub fn encode_repair_request(req: &PeerRepairRequest) -> Vec<u8> {
157 let mut map = Map::new();
158 map.insert(KEY_REQUEST_ID, CBOR::to_byte_string(req.request_id));
159 map.insert(KEY_NAMED_PATH, req.named_path.clone());
160 prepend_frame(FrameType::RepairRequest, CBOR::from(map).to_cbor_data())
161}
162
163pub fn decode_repair_request(bytes: &[u8]) -> Result<PeerRepairRequest, ProtocolError> {
164 let body = strip_frame(FrameType::RepairRequest, bytes)?;
165 let map = parse_map(body)?;
166 Ok(PeerRepairRequest {
167 request_id: extract_fixed::<16>(&map, KEY_REQUEST_ID, "request_id")?,
168 named_path: extract_text(&map, KEY_NAMED_PATH)?,
169 })
170}
171
172pub fn encode_repair_response(resp: &PeerRepairResponse) -> Vec<u8> {
173 let mut map = Map::new();
174 map.insert(KEY_REQUEST_ID, CBOR::to_byte_string(resp.request_id));
175 map.insert(KEY_OBJECT_BYTES, CBOR::to_byte_string(&resp.object_bytes));
176 prepend_frame(FrameType::RepairResponse, CBOR::from(map).to_cbor_data())
177}
178
179pub fn decode_repair_response(bytes: &[u8]) -> Result<PeerRepairResponse, ProtocolError> {
180 let body = strip_frame(FrameType::RepairResponse, bytes)?;
181 let map = parse_map(body)?;
182 Ok(PeerRepairResponse {
183 request_id: extract_fixed::<16>(&map, KEY_REQUEST_ID, "request_id")?,
184 object_bytes: extract_bytes(&map, KEY_OBJECT_BYTES)?,
185 })
186}
187
188pub fn encode_repair_not_found(nf: &PeerRepairNotFound) -> Vec<u8> {
189 let mut map = Map::new();
190 map.insert(KEY_REQUEST_ID, CBOR::to_byte_string(nf.request_id));
191 prepend_frame(FrameType::RepairNotFound, CBOR::from(map).to_cbor_data())
192}
193
194pub fn decode_repair_not_found(bytes: &[u8]) -> Result<PeerRepairNotFound, ProtocolError> {
195 let body = strip_frame(FrameType::RepairNotFound, bytes)?;
196 let map = parse_map(body)?;
197 Ok(PeerRepairNotFound {
198 request_id: extract_fixed::<16>(&map, KEY_REQUEST_ID, "request_id")?,
199 })
200}
201
202pub fn encode_attachment_chunk(chunk: &AttachmentChunk) -> Vec<u8> {
203 let mut map = Map::new();
204 map.insert(KEY_ATTACHMENT_ID, CBOR::to_byte_string(chunk.attachment_id));
205 map.insert(KEY_CHUNK_INDEX, u64::from(chunk.chunk_index));
206 map.insert(KEY_TOTAL_CHUNKS, u64::from(chunk.total_chunks));
207 map.insert(KEY_DATA, CBOR::to_byte_string(&chunk.data));
208 prepend_frame(FrameType::AttachmentChunk, CBOR::from(map).to_cbor_data())
209}
210
211pub fn decode_attachment_chunk(bytes: &[u8]) -> Result<AttachmentChunk, ProtocolError> {
212 let body = strip_frame(FrameType::AttachmentChunk, bytes)?;
213 let map = parse_map(body)?;
214 Ok(AttachmentChunk {
215 attachment_id: extract_fixed::<16>(&map, KEY_ATTACHMENT_ID, "attachment_id")?,
216 chunk_index: extract_u32(&map, KEY_CHUNK_INDEX, "chunk_index")?,
217 total_chunks: extract_u32(&map, KEY_TOTAL_CHUNKS, "total_chunks")?,
218 data: extract_bytes(&map, KEY_DATA)?,
219 })
220}
221
222pub fn encode_attachment_chunk_ack(ack: &AttachmentChunkAck) -> Vec<u8> {
223 let mut map = Map::new();
224 map.insert(KEY_ATTACHMENT_ID, CBOR::to_byte_string(ack.attachment_id));
225 map.insert(KEY_CHUNK_INDEX, u64::from(ack.chunk_index));
226 prepend_frame(
227 FrameType::AttachmentChunkAck,
228 CBOR::from(map).to_cbor_data(),
229 )
230}
231
232pub fn decode_attachment_chunk_ack(bytes: &[u8]) -> Result<AttachmentChunkAck, ProtocolError> {
233 let body = strip_frame(FrameType::AttachmentChunkAck, bytes)?;
234 let map = parse_map(body)?;
235 Ok(AttachmentChunkAck {
236 attachment_id: extract_fixed::<16>(&map, KEY_ATTACHMENT_ID, "attachment_id")?,
237 chunk_index: extract_u32(&map, KEY_CHUNK_INDEX, "chunk_index")?,
238 })
239}
240
241pub fn encode_attachment_abort(abort: &AttachmentAbort) -> Vec<u8> {
242 let mut map = Map::new();
243 map.insert(KEY_ATTACHMENT_ID, CBOR::to_byte_string(abort.attachment_id));
244 map.insert(KEY_REASON, abort.reason.clone());
245 prepend_frame(FrameType::AttachmentAbort, CBOR::from(map).to_cbor_data())
246}
247
248pub fn decode_attachment_abort(bytes: &[u8]) -> Result<AttachmentAbort, ProtocolError> {
249 let body = strip_frame(FrameType::AttachmentAbort, bytes)?;
250 let map = parse_map(body)?;
251 Ok(AttachmentAbort {
252 attachment_id: extract_fixed::<16>(&map, KEY_ATTACHMENT_ID, "attachment_id")?,
253 reason: extract_text(&map, KEY_REASON)?,
254 })
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn test_frame_type_identifies_all_variants() {
263 let cases: &[(u8, FrameType)] = &[
264 (0x01, FrameType::RepairRequest),
265 (0x02, FrameType::RepairResponse),
266 (0x03, FrameType::RepairNotFound),
267 (0x10, FrameType::SyncManifest),
268 (0x11, FrameType::SyncChunk),
269 (0x12, FrameType::SyncAck),
270 (0x20, FrameType::AttachmentChunk),
271 (0x21, FrameType::AttachmentChunkAck),
272 (0x22, FrameType::AttachmentAbort),
273 ];
274 for &(byte, expected) in cases {
275 assert_eq!(
276 frame_type(&[byte, 0x00]),
277 Some(expected),
278 "byte 0x{byte:02x}"
279 );
280 }
281 }
282
283 #[test]
284 fn test_frame_type_returns_none_for_unknown_byte() {
285 assert_eq!(frame_type(&[0xFF]), None);
286 assert_eq!(frame_type(&[0x00]), None);
287 assert_eq!(frame_type(&[0x04]), None);
288 }
289
290 #[test]
291 fn test_frame_type_returns_none_for_empty_slice() {
292 assert_eq!(frame_type(&[]), None);
293 }
294
295 #[test]
296 fn test_repair_request_round_trip() {
297 let req = PeerRepairRequest {
298 request_id: [0xAB; 16],
299 named_path: "mesh/room/deadbeef/object/cafebabe/1".to_string(),
300 };
301 let encoded = encode_repair_request(&req);
302 assert_eq!(encoded[0], FrameType::RepairRequest as u8);
303 let decoded = decode_repair_request(&encoded).expect("decode must succeed");
304 assert_eq!(decoded, req);
305 }
306
307 #[test]
308 fn test_repair_response_round_trip() {
309 let resp = PeerRepairResponse {
310 request_id: [0x01; 16],
311 object_bytes: vec![0xDE, 0xAD, 0xBE, 0xEF],
312 };
313 let encoded = encode_repair_response(&resp);
314 assert_eq!(encoded[0], FrameType::RepairResponse as u8);
315 let decoded = decode_repair_response(&encoded).expect("decode must succeed");
316 assert_eq!(decoded, resp);
317 }
318
319 #[test]
320 fn test_repair_not_found_round_trip() {
321 let nf = PeerRepairNotFound {
322 request_id: [0xFF; 16],
323 };
324 let encoded = encode_repair_not_found(&nf);
325 assert_eq!(encoded[0], FrameType::RepairNotFound as u8);
326 let decoded = decode_repair_not_found(&encoded).expect("decode must succeed");
327 assert_eq!(decoded, nf);
328 }
329
330 #[test]
331 fn test_attachment_chunk_round_trip() {
332 let chunk = AttachmentChunk {
333 attachment_id: [0xCC; 16],
334 chunk_index: 3,
335 total_chunks: 10,
336 data: vec![1, 2, 3, 4, 5],
337 };
338 let encoded = encode_attachment_chunk(&chunk);
339 assert_eq!(encoded[0], FrameType::AttachmentChunk as u8);
340 let decoded = decode_attachment_chunk(&encoded).expect("decode must succeed");
341 assert_eq!(decoded, chunk);
342 }
343
344 #[test]
345 fn test_attachment_chunk_ack_round_trip() {
346 let ack = AttachmentChunkAck {
347 attachment_id: [0xDD; 16],
348 chunk_index: 7,
349 };
350 let encoded = encode_attachment_chunk_ack(&ack);
351 assert_eq!(encoded[0], FrameType::AttachmentChunkAck as u8);
352 let decoded = decode_attachment_chunk_ack(&encoded).expect("decode must succeed");
353 assert_eq!(decoded, ack);
354 }
355
356 #[test]
357 fn test_attachment_abort_round_trip() {
358 let abort = AttachmentAbort {
359 attachment_id: [0xEE; 16],
360 reason: "transfer cancelled by sender".to_string(),
361 };
362 let encoded = encode_attachment_abort(&abort);
363 assert_eq!(encoded[0], FrameType::AttachmentAbort as u8);
364 let decoded = decode_attachment_abort(&encoded).expect("decode must succeed");
365 assert_eq!(decoded, abort);
366 }
367}