Skip to main content

protocol_core/
repair_peer.rs

1//! Peer-to-peer repair request/response framing.
2//!
3//! All peer transport messages share a 1-byte frame type prefix followed by
4//! a dCBOR body. The consumer dispatches on the first byte.
5
6use 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/// Frame type prefix byte for mesh peer transport messages.
20#[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
71/// Parse the frame type from the first byte. Returns `None` for unknown types.
72pub 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}