use dcbor::prelude::*;
use crate::ProtocolError;
const KEY_REQUEST_ID: u64 = 0;
const KEY_NAMED_PATH: u64 = 1;
const KEY_OBJECT_BYTES: u64 = 2;
const KEY_ATTACHMENT_ID: u64 = 0;
const KEY_CHUNK_INDEX: u64 = 1;
const KEY_TOTAL_CHUNKS: u64 = 2;
const KEY_DATA: u64 = 3;
const KEY_REASON: u64 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum FrameType {
RepairRequest = 0x01,
RepairResponse = 0x02,
RepairNotFound = 0x03,
SyncManifest = 0x10,
SyncChunk = 0x11,
SyncAck = 0x12,
AttachmentChunk = 0x20,
AttachmentChunkAck = 0x21,
AttachmentAbort = 0x22,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerRepairRequest {
pub request_id: [u8; 16],
pub named_path: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerRepairResponse {
pub request_id: [u8; 16],
pub object_bytes: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeerRepairNotFound {
pub request_id: [u8; 16],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttachmentChunk {
pub attachment_id: [u8; 16],
pub chunk_index: u32,
pub total_chunks: u32,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttachmentChunkAck {
pub attachment_id: [u8; 16],
pub chunk_index: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttachmentAbort {
pub attachment_id: [u8; 16],
pub reason: String,
}
pub fn frame_type(bytes: &[u8]) -> Option<FrameType> {
let first = *bytes.first()?;
match first {
0x01 => Some(FrameType::RepairRequest),
0x02 => Some(FrameType::RepairResponse),
0x03 => Some(FrameType::RepairNotFound),
0x10 => Some(FrameType::SyncManifest),
0x11 => Some(FrameType::SyncChunk),
0x12 => Some(FrameType::SyncAck),
0x20 => Some(FrameType::AttachmentChunk),
0x21 => Some(FrameType::AttachmentChunkAck),
0x22 => Some(FrameType::AttachmentAbort),
_ => None,
}
}
fn prepend_frame(frame: FrameType, body: Vec<u8>) -> Vec<u8> {
let mut out = Vec::with_capacity(1 + body.len());
out.push(frame as u8);
out.extend_from_slice(&body);
out
}
fn strip_frame(expected: FrameType, bytes: &[u8]) -> Result<&[u8], ProtocolError> {
match bytes.first() {
None => Err(ProtocolError::InvalidEncoding("empty frame".to_string())),
Some(&b) if b != expected as u8 => Err(ProtocolError::InvalidEncoding(format!(
"expected frame type 0x{:02x}, got 0x{:02x}",
expected as u8, b
))),
_ => Ok(&bytes[1..]),
}
}
fn parse_map(bytes: &[u8]) -> Result<Map, ProtocolError> {
let cbor =
CBOR::try_from_data(bytes).map_err(|e| ProtocolError::InvalidEncoding(e.to_string()))?;
cbor.try_into_map()
.map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))
}
fn extract_fixed<const N: usize>(
map: &Map,
key: u64,
field: &str,
) -> Result<[u8; N], ProtocolError> {
let cbor: CBOR = map
.extract(key)
.map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))?;
let bytes = cbor
.try_into_byte_string()
.map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))?;
bytes
.try_into()
.map_err(|_| ProtocolError::InvalidEnvelope(format!("{field} must be {N} bytes")))
}
fn extract_bytes(map: &Map, key: u64) -> Result<Vec<u8>, ProtocolError> {
let cbor: CBOR = map
.extract(key)
.map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))?;
cbor.try_into_byte_string()
.map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))
}
fn extract_u64(map: &Map, key: u64) -> Result<u64, ProtocolError> {
map.extract(key)
.map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))
}
fn extract_u32(map: &Map, key: u64, field: &str) -> Result<u32, ProtocolError> {
let value = extract_u64(map, key)?;
u32::try_from(value)
.map_err(|_| ProtocolError::InvalidEnvelope(format!("{field} exceeds u32 range: {value}")))
}
fn extract_text(map: &Map, key: u64) -> Result<String, ProtocolError> {
let cbor: CBOR = map
.extract(key)
.map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))?;
cbor.try_into_text()
.map_err(|e| ProtocolError::InvalidEnvelope(e.to_string()))
}
pub fn encode_repair_request(req: &PeerRepairRequest) -> Vec<u8> {
let mut map = Map::new();
map.insert(KEY_REQUEST_ID, CBOR::to_byte_string(req.request_id));
map.insert(KEY_NAMED_PATH, req.named_path.clone());
prepend_frame(FrameType::RepairRequest, CBOR::from(map).to_cbor_data())
}
pub fn decode_repair_request(bytes: &[u8]) -> Result<PeerRepairRequest, ProtocolError> {
let body = strip_frame(FrameType::RepairRequest, bytes)?;
let map = parse_map(body)?;
Ok(PeerRepairRequest {
request_id: extract_fixed::<16>(&map, KEY_REQUEST_ID, "request_id")?,
named_path: extract_text(&map, KEY_NAMED_PATH)?,
})
}
pub fn encode_repair_response(resp: &PeerRepairResponse) -> Vec<u8> {
let mut map = Map::new();
map.insert(KEY_REQUEST_ID, CBOR::to_byte_string(resp.request_id));
map.insert(KEY_OBJECT_BYTES, CBOR::to_byte_string(&resp.object_bytes));
prepend_frame(FrameType::RepairResponse, CBOR::from(map).to_cbor_data())
}
pub fn decode_repair_response(bytes: &[u8]) -> Result<PeerRepairResponse, ProtocolError> {
let body = strip_frame(FrameType::RepairResponse, bytes)?;
let map = parse_map(body)?;
Ok(PeerRepairResponse {
request_id: extract_fixed::<16>(&map, KEY_REQUEST_ID, "request_id")?,
object_bytes: extract_bytes(&map, KEY_OBJECT_BYTES)?,
})
}
pub fn encode_repair_not_found(nf: &PeerRepairNotFound) -> Vec<u8> {
let mut map = Map::new();
map.insert(KEY_REQUEST_ID, CBOR::to_byte_string(nf.request_id));
prepend_frame(FrameType::RepairNotFound, CBOR::from(map).to_cbor_data())
}
pub fn decode_repair_not_found(bytes: &[u8]) -> Result<PeerRepairNotFound, ProtocolError> {
let body = strip_frame(FrameType::RepairNotFound, bytes)?;
let map = parse_map(body)?;
Ok(PeerRepairNotFound {
request_id: extract_fixed::<16>(&map, KEY_REQUEST_ID, "request_id")?,
})
}
pub fn encode_attachment_chunk(chunk: &AttachmentChunk) -> Vec<u8> {
let mut map = Map::new();
map.insert(KEY_ATTACHMENT_ID, CBOR::to_byte_string(chunk.attachment_id));
map.insert(KEY_CHUNK_INDEX, u64::from(chunk.chunk_index));
map.insert(KEY_TOTAL_CHUNKS, u64::from(chunk.total_chunks));
map.insert(KEY_DATA, CBOR::to_byte_string(&chunk.data));
prepend_frame(FrameType::AttachmentChunk, CBOR::from(map).to_cbor_data())
}
pub fn decode_attachment_chunk(bytes: &[u8]) -> Result<AttachmentChunk, ProtocolError> {
let body = strip_frame(FrameType::AttachmentChunk, bytes)?;
let map = parse_map(body)?;
Ok(AttachmentChunk {
attachment_id: extract_fixed::<16>(&map, KEY_ATTACHMENT_ID, "attachment_id")?,
chunk_index: extract_u32(&map, KEY_CHUNK_INDEX, "chunk_index")?,
total_chunks: extract_u32(&map, KEY_TOTAL_CHUNKS, "total_chunks")?,
data: extract_bytes(&map, KEY_DATA)?,
})
}
pub fn encode_attachment_chunk_ack(ack: &AttachmentChunkAck) -> Vec<u8> {
let mut map = Map::new();
map.insert(KEY_ATTACHMENT_ID, CBOR::to_byte_string(ack.attachment_id));
map.insert(KEY_CHUNK_INDEX, u64::from(ack.chunk_index));
prepend_frame(
FrameType::AttachmentChunkAck,
CBOR::from(map).to_cbor_data(),
)
}
pub fn decode_attachment_chunk_ack(bytes: &[u8]) -> Result<AttachmentChunkAck, ProtocolError> {
let body = strip_frame(FrameType::AttachmentChunkAck, bytes)?;
let map = parse_map(body)?;
Ok(AttachmentChunkAck {
attachment_id: extract_fixed::<16>(&map, KEY_ATTACHMENT_ID, "attachment_id")?,
chunk_index: extract_u32(&map, KEY_CHUNK_INDEX, "chunk_index")?,
})
}
pub fn encode_attachment_abort(abort: &AttachmentAbort) -> Vec<u8> {
let mut map = Map::new();
map.insert(KEY_ATTACHMENT_ID, CBOR::to_byte_string(abort.attachment_id));
map.insert(KEY_REASON, abort.reason.clone());
prepend_frame(FrameType::AttachmentAbort, CBOR::from(map).to_cbor_data())
}
pub fn decode_attachment_abort(bytes: &[u8]) -> Result<AttachmentAbort, ProtocolError> {
let body = strip_frame(FrameType::AttachmentAbort, bytes)?;
let map = parse_map(body)?;
Ok(AttachmentAbort {
attachment_id: extract_fixed::<16>(&map, KEY_ATTACHMENT_ID, "attachment_id")?,
reason: extract_text(&map, KEY_REASON)?,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frame_type_identifies_all_variants() {
let cases: &[(u8, FrameType)] = &[
(0x01, FrameType::RepairRequest),
(0x02, FrameType::RepairResponse),
(0x03, FrameType::RepairNotFound),
(0x10, FrameType::SyncManifest),
(0x11, FrameType::SyncChunk),
(0x12, FrameType::SyncAck),
(0x20, FrameType::AttachmentChunk),
(0x21, FrameType::AttachmentChunkAck),
(0x22, FrameType::AttachmentAbort),
];
for &(byte, expected) in cases {
assert_eq!(
frame_type(&[byte, 0x00]),
Some(expected),
"byte 0x{byte:02x}"
);
}
}
#[test]
fn test_frame_type_returns_none_for_unknown_byte() {
assert_eq!(frame_type(&[0xFF]), None);
assert_eq!(frame_type(&[0x00]), None);
assert_eq!(frame_type(&[0x04]), None);
}
#[test]
fn test_frame_type_returns_none_for_empty_slice() {
assert_eq!(frame_type(&[]), None);
}
#[test]
fn test_repair_request_round_trip() {
let req = PeerRepairRequest {
request_id: [0xAB; 16],
named_path: "mesh/room/deadbeef/object/cafebabe/1".to_string(),
};
let encoded = encode_repair_request(&req);
assert_eq!(encoded[0], FrameType::RepairRequest as u8);
let decoded = decode_repair_request(&encoded).expect("decode must succeed");
assert_eq!(decoded, req);
}
#[test]
fn test_repair_response_round_trip() {
let resp = PeerRepairResponse {
request_id: [0x01; 16],
object_bytes: vec![0xDE, 0xAD, 0xBE, 0xEF],
};
let encoded = encode_repair_response(&resp);
assert_eq!(encoded[0], FrameType::RepairResponse as u8);
let decoded = decode_repair_response(&encoded).expect("decode must succeed");
assert_eq!(decoded, resp);
}
#[test]
fn test_repair_not_found_round_trip() {
let nf = PeerRepairNotFound {
request_id: [0xFF; 16],
};
let encoded = encode_repair_not_found(&nf);
assert_eq!(encoded[0], FrameType::RepairNotFound as u8);
let decoded = decode_repair_not_found(&encoded).expect("decode must succeed");
assert_eq!(decoded, nf);
}
#[test]
fn test_attachment_chunk_round_trip() {
let chunk = AttachmentChunk {
attachment_id: [0xCC; 16],
chunk_index: 3,
total_chunks: 10,
data: vec![1, 2, 3, 4, 5],
};
let encoded = encode_attachment_chunk(&chunk);
assert_eq!(encoded[0], FrameType::AttachmentChunk as u8);
let decoded = decode_attachment_chunk(&encoded).expect("decode must succeed");
assert_eq!(decoded, chunk);
}
#[test]
fn test_attachment_chunk_ack_round_trip() {
let ack = AttachmentChunkAck {
attachment_id: [0xDD; 16],
chunk_index: 7,
};
let encoded = encode_attachment_chunk_ack(&ack);
assert_eq!(encoded[0], FrameType::AttachmentChunkAck as u8);
let decoded = decode_attachment_chunk_ack(&encoded).expect("decode must succeed");
assert_eq!(decoded, ack);
}
#[test]
fn test_attachment_abort_round_trip() {
let abort = AttachmentAbort {
attachment_id: [0xEE; 16],
reason: "transfer cancelled by sender".to_string(),
};
let encoded = encode_attachment_abort(&abort);
assert_eq!(encoded[0], FrameType::AttachmentAbort as u8);
let decoded = decode_attachment_abort(&encoded).expect("decode must succeed");
assert_eq!(decoded, abort);
}
}