lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit fc09360cfd4e161e13e72127e350852dcf47c18a
parent 97d31a05a998aa1304908b825a2bd31de03df2e5
Author: triesap <tyson@radroots.org>
Date:   Wed,  8 Jul 2026 20:39:51 +0000

mesh: align frames with Transport V1 CDDL

- replace the event-head wire shape with the seven-field mesh-frame-v1 contract
- add typed frame kinds, message ids, TTL validation, and empty-map MVP payloads
- reject the previous five-field CBOR shape and forbidden payload bytes
- cover checked integer decoding and deterministic frame round trips in tests

Diffstat:
Mcrates/mesh/src/cbor.rs | 140++++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcrates/mesh/src/error.rs | 8++++++--
Mcrates/mesh/src/lib.rs | 4++--
Mcrates/mesh/src/model.rs | 116++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mcrates/mesh/tests/mesh.rs | 232++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
5 files changed, 326 insertions(+), 174 deletions(-)

diff --git a/crates/mesh/src/cbor.rs b/crates/mesh/src/cbor.rs @@ -1,6 +1,6 @@ use crate::{ - RADROOTS_MESH_FRAME_VERSION, RadrootsMeshError, RadrootsMeshEventHead, RadrootsMeshFrame, - RadrootsMeshPayloadPolicy, RadrootsMeshScope, + RADROOTS_MESH_FRAME_VERSION, RadrootsMeshError, RadrootsMeshFrame, RadrootsMeshFrameType, + RadrootsMeshPayload, RadrootsMeshScope, }; use alloc::string::String; use alloc::vec::Vec; @@ -8,103 +8,83 @@ use alloc::vec::Vec; pub fn encode_mesh_frame_cbor(frame: &RadrootsMeshFrame) -> Result<Vec<u8>, RadrootsMeshError> { frame.validate()?; let mut output = Vec::new(); - encode_map_len(&mut output, 5); - encode_uint(&mut output, 1); + encode_map_len(&mut output, 7); + encode_uint(&mut output, 0); encode_uint(&mut output, u64::from(frame.version)); + encode_uint(&mut output, 1); + encode_uint(&mut output, frame.frame_type.code()); encode_uint(&mut output, 2); - encode_text(&mut output, &frame.scope.cbor_label())?; + encode_text(&mut output, &frame.scope_id.cbor_label())?; encode_uint(&mut output, 3); - encode_text(&mut output, frame.payload_policy.label())?; + encode_text(&mut output, &frame.message_id)?; encode_uint(&mut output, 4); - encode_array_len(&mut output, frame.event_heads.len() as u64); - for head in &frame.event_heads { - encode_event_head(&mut output, head)?; - } + encode_uint(&mut output, frame.created_at_ms); encode_uint(&mut output, 5); - output.push(0xf6); + encode_uint(&mut output, frame.ttl); + encode_uint(&mut output, 6); + encode_payload(&mut output, &frame.payload)?; Ok(output) } pub fn decode_mesh_frame_cbor(bytes: &[u8]) -> Result<RadrootsMeshFrame, RadrootsMeshError> { let mut cursor = Cursor::new(bytes); - cursor.expect_map_len(5)?; - cursor.expect_uint(1)?; - let version = cursor.read_uint()? as u16; + cursor.expect_map_len(7)?; + cursor.expect_uint(0)?; + let version = cursor.read_uint_u16()?; if version != RADROOTS_MESH_FRAME_VERSION { return Err(RadrootsMeshError::UnsupportedVersion); } + cursor.expect_uint(1)?; + let frame_type = RadrootsMeshFrameType::parse_code(cursor.read_uint()?)?; cursor.expect_uint(2)?; - let scope = RadrootsMeshScope::parse(&cursor.read_text()?)?; + let scope_id = RadrootsMeshScope::parse(&cursor.read_text()?)?; cursor.expect_uint(3)?; - let payload_policy = RadrootsMeshPayloadPolicy::parse(&cursor.read_text()?)?; + let message_id = cursor.read_text()?; cursor.expect_uint(4)?; - let head_count = cursor.read_array_len()? as usize; - let mut event_heads = Vec::with_capacity(head_count); - for _ in 0..head_count { - event_heads.push(decode_event_head(&mut cursor)?); - } + let created_at_ms = cursor.read_uint()?; cursor.expect_uint(5)?; - cursor.expect_null()?; + let ttl = cursor.read_uint()?; + cursor.expect_uint(6)?; + let payload = cursor.read_payload()?; cursor.finish()?; let frame = RadrootsMeshFrame { version, - scope, - payload_policy, - event_heads, - payload: None, + frame_type, + scope_id, + message_id, + created_at_ms, + ttl, + payload, }; frame.validate()?; Ok(frame) } -fn encode_event_head( +fn encode_payload( output: &mut Vec<u8>, - head: &RadrootsMeshEventHead, + payload: &RadrootsMeshPayload, ) -> Result<(), RadrootsMeshError> { - encode_map_len(output, 4); - encode_uint(output, 1); - encode_text(output, &head.event_id)?; - encode_uint(output, 2); - encode_text(output, &head.author)?; - encode_uint(output, 3); - encode_uint(output, u64::from(head.kind)); - encode_uint(output, 4); - encode_uint(output, head.created_at); + payload.validate()?; + match payload { + RadrootsMeshPayload::EmptyMap => encode_map_len(output, 0), + RadrootsMeshPayload::Bytes(_) => { + return Err(RadrootsMeshError::PayloadTransmissionForbidden); + } + } Ok(()) } -fn decode_event_head(cursor: &mut Cursor<'_>) -> Result<RadrootsMeshEventHead, RadrootsMeshError> { - cursor.expect_map_len(4)?; - cursor.expect_uint(1)?; - let event_id = cursor.read_text()?; - cursor.expect_uint(2)?; - let author = cursor.read_text()?; - cursor.expect_uint(3)?; - let kind = cursor.read_uint()? as u32; - cursor.expect_uint(4)?; - let created_at = cursor.read_uint()?; - Ok(RadrootsMeshEventHead { - event_id, - author, - kind, - created_at, - }) -} - fn encode_uint(output: &mut Vec<u8>, value: u64) { encode_major(output, 0, value); } fn encode_text(output: &mut Vec<u8>, value: &str) -> Result<(), RadrootsMeshError> { - encode_major(output, 3, value.len() as u64); + let len = u64::try_from(value.len()).map_err(|_| RadrootsMeshError::InvalidCbor)?; + encode_major(output, 3, len); output.extend_from_slice(value.as_bytes()); Ok(()) } -fn encode_array_len(output: &mut Vec<u8>, len: u64) { - encode_major(output, 4, len); -} - fn encode_map_len(output: &mut Vec<u8>, len: u64) { encode_major(output, 5, len); } @@ -194,6 +174,10 @@ impl<'a> Cursor<'a> { self.read_major(0) } + fn read_uint_u16(&mut self) -> Result<u16, RadrootsMeshError> { + u16::try_from(self.read_uint()?).map_err(|_| RadrootsMeshError::InvalidCbor) + } + fn expect_uint(&mut self, expected: u64) -> Result<(), RadrootsMeshError> { if self.read_uint()? == expected { Ok(()) @@ -203,25 +187,41 @@ impl<'a> Cursor<'a> { } fn read_text(&mut self) -> Result<String, RadrootsMeshError> { - let len = self.read_major(3)? as usize; + let len = + usize::try_from(self.read_major(3)?).map_err(|_| RadrootsMeshError::InvalidCbor)?; let bytes = self.read_exact(len)?; String::from_utf8(bytes.to_vec()).map_err(|_| RadrootsMeshError::InvalidUtf8) } - fn read_array_len(&mut self) -> Result<u64, RadrootsMeshError> { - self.read_major(4) + fn skip_bytes(&mut self) -> Result<(), RadrootsMeshError> { + let len = + usize::try_from(self.read_major(2)?).map_err(|_| RadrootsMeshError::InvalidCbor)?; + self.read_exact(len)?; + Ok(()) } - fn expect_map_len(&mut self, expected: u64) -> Result<(), RadrootsMeshError> { - if self.read_major(5)? == expected { - Ok(()) - } else { - Err(RadrootsMeshError::InvalidCbor) + fn read_payload(&mut self) -> Result<RadrootsMeshPayload, RadrootsMeshError> { + let initial = self.read_byte()?; + match initial >> 5 { + 2 => { + self.offset -= 1; + self.skip_bytes()?; + Err(RadrootsMeshError::PayloadTransmissionForbidden) + } + 5 => { + self.offset -= 1; + if self.read_major(5)? == 0 { + Ok(RadrootsMeshPayload::EmptyMap) + } else { + Err(RadrootsMeshError::PayloadTransmissionForbidden) + } + } + _ => Err(RadrootsMeshError::InvalidCbor), } } - fn expect_null(&mut self) -> Result<(), RadrootsMeshError> { - if self.read_byte()? == 0xf6 { + fn expect_map_len(&mut self, expected: u64) -> Result<(), RadrootsMeshError> { + if self.read_major(5)? == expected { Ok(()) } else { Err(RadrootsMeshError::InvalidCbor) diff --git a/crates/mesh/src/error.rs b/crates/mesh/src/error.rs @@ -3,11 +3,13 @@ use core::fmt; #[derive(Clone, Debug, PartialEq, Eq)] pub enum RadrootsMeshError { EmptyCustomScope, + EmptyMessageId, + InvalidTtl, PayloadTransmissionForbidden, InvalidCbor, InvalidUtf8, + UnknownFrameType, UnknownScope, - UnknownPayloadPolicy, UnsupportedVersion, } @@ -15,13 +17,15 @@ impl fmt::Display for RadrootsMeshError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::EmptyCustomScope => f.write_str("mesh custom scope is empty"), + Self::EmptyMessageId => f.write_str("mesh message id is empty"), + Self::InvalidTtl => f.write_str("mesh frame TTL is invalid"), Self::PayloadTransmissionForbidden => { f.write_str("mesh payload transmission is forbidden") } Self::InvalidCbor => f.write_str("mesh frame CBOR is invalid"), Self::InvalidUtf8 => f.write_str("mesh frame text is invalid UTF-8"), + Self::UnknownFrameType => f.write_str("mesh frame type is unknown"), Self::UnknownScope => f.write_str("mesh scope is unknown"), - Self::UnknownPayloadPolicy => f.write_str("mesh payload policy is unknown"), Self::UnsupportedVersion => f.write_str("mesh frame version is unsupported"), } } diff --git a/crates/mesh/src/lib.rs b/crates/mesh/src/lib.rs @@ -10,6 +10,6 @@ mod model; pub use cbor::{decode_mesh_frame_cbor, encode_mesh_frame_cbor}; pub use error::RadrootsMeshError; pub use model::{ - RADROOTS_MESH_FRAME_VERSION, RadrootsMeshEventHead, RadrootsMeshFrame, - RadrootsMeshPayloadPolicy, RadrootsMeshScope, + RADROOTS_MESH_FRAME_VERSION, RadrootsMeshFrame, RadrootsMeshFrameType, RadrootsMeshPayload, + RadrootsMeshScope, }; diff --git a/crates/mesh/src/model.rs b/crates/mesh/src/model.rs @@ -5,6 +5,53 @@ use alloc::vec::Vec; pub const RADROOTS_MESH_FRAME_VERSION: u16 = 1; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RadrootsMeshFrameType { + Hello, + EventHeadAnnounce, + EventRequest, + EventChunk, + EventAck, + RouteProbe, +} + +impl RadrootsMeshFrameType { + pub fn code(self) -> u64 { + match self { + Self::Hello => 0, + Self::EventHeadAnnounce => 1, + Self::EventRequest => 2, + Self::EventChunk => 3, + Self::EventAck => 4, + Self::RouteProbe => 5, + } + } + + pub fn parse_code(value: u64) -> Result<Self, RadrootsMeshError> { + match value { + 0 => Ok(Self::Hello), + 1 => Ok(Self::EventHeadAnnounce), + 2 => Ok(Self::EventRequest), + 3 => Ok(Self::EventChunk), + 4 => Ok(Self::EventAck), + 5 => Ok(Self::RouteProbe), + _ => Err(RadrootsMeshError::UnknownFrameType), + } + } + + pub fn label(self) -> &'static str { + match self { + Self::Hello => "hello", + Self::EventHeadAnnounce => "event_head_announce", + Self::EventRequest => "event_request", + Self::EventChunk => "event_chunk", + Self::EventAck => "event_ack", + Self::RouteProbe => "route_probe", + } + } +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum RadrootsMeshScope { Local, @@ -52,53 +99,53 @@ impl RadrootsMeshScope { } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum RadrootsMeshPayloadPolicy { - PayloadTransmissionForbidden, +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum RadrootsMeshPayload { + EmptyMap, + Bytes(Vec<u8>), } -impl RadrootsMeshPayloadPolicy { - pub fn label(self) -> &'static str { - match self { - Self::PayloadTransmissionForbidden => "payload-forbidden", - } +impl RadrootsMeshPayload { + pub fn empty() -> Self { + Self::EmptyMap } - pub fn parse(value: &str) -> Result<Self, RadrootsMeshError> { - match value { - "payload-forbidden" => Ok(Self::PayloadTransmissionForbidden), - _ => Err(RadrootsMeshError::UnknownPayloadPolicy), + pub fn validate(&self) -> Result<(), RadrootsMeshError> { + match self { + Self::EmptyMap => Ok(()), + Self::Bytes(_) => Err(RadrootsMeshError::PayloadTransmissionForbidden), } } } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RadrootsMeshEventHead { - pub event_id: String, - pub author: String, - pub kind: u32, - pub created_at: u64, -} - -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsMeshFrame { pub version: u16, - pub scope: RadrootsMeshScope, - pub payload_policy: RadrootsMeshPayloadPolicy, - pub event_heads: Vec<RadrootsMeshEventHead>, - pub payload: Option<Vec<u8>>, + pub frame_type: RadrootsMeshFrameType, + pub scope_id: RadrootsMeshScope, + pub message_id: String, + pub created_at_ms: u64, + pub ttl: u64, + pub payload: RadrootsMeshPayload, } impl RadrootsMeshFrame { - pub fn new(scope: RadrootsMeshScope, event_heads: Vec<RadrootsMeshEventHead>) -> Self { + pub fn new( + frame_type: RadrootsMeshFrameType, + scope_id: RadrootsMeshScope, + message_id: impl Into<String>, + created_at_ms: u64, + ttl: u64, + ) -> Self { Self { version: RADROOTS_MESH_FRAME_VERSION, - scope, - payload_policy: RadrootsMeshPayloadPolicy::PayloadTransmissionForbidden, - event_heads, - payload: None, + frame_type, + scope_id, + message_id: message_id.into(), + created_at_ms, + ttl, + payload: RadrootsMeshPayload::empty(), } } @@ -106,9 +153,12 @@ impl RadrootsMeshFrame { if self.version != RADROOTS_MESH_FRAME_VERSION { return Err(RadrootsMeshError::UnsupportedVersion); } - if self.payload.is_some() { - return Err(RadrootsMeshError::PayloadTransmissionForbidden); + if self.message_id.trim().is_empty() { + return Err(RadrootsMeshError::EmptyMessageId); + } + if self.ttl == 0 { + return Err(RadrootsMeshError::InvalidTtl); } - Ok(()) + self.payload.validate() } } diff --git a/crates/mesh/tests/mesh.rs b/crates/mesh/tests/mesh.rs @@ -1,19 +1,29 @@ use radroots_mesh::{ - RadrootsMeshError, RadrootsMeshEventHead, RadrootsMeshFrame, RadrootsMeshPayloadPolicy, + RadrootsMeshError, RadrootsMeshFrame, RadrootsMeshFrameType, RadrootsMeshPayload, RadrootsMeshScope, decode_mesh_frame_cbor, encode_mesh_frame_cbor, }; +fn default_frame() -> RadrootsMeshFrame { + RadrootsMeshFrame::new( + RadrootsMeshFrameType::Hello, + RadrootsMeshScope::Local, + "message-1", + 42, + 60_000, + ) +} + #[test] -fn default_frame_encodes_as_deterministic_cbor() { - let frame = RadrootsMeshFrame::new(RadrootsMeshScope::Local, Vec::new()); +fn default_frame_encodes_as_mesh_frame_v1_cddl_cbor() { + let frame = default_frame(); let encoded = encode_mesh_frame_cbor(&frame).expect("encode frame"); assert_eq!( encoded, [ - 0xa5, 0x01, 0x01, 0x02, 0x65, b'l', b'o', b'c', b'a', b'l', 0x03, 0x71, b'p', b'a', - b'y', b'l', b'o', b'a', b'd', b'-', b'f', b'o', b'r', b'b', b'i', b'd', b'd', b'e', - b'n', 0x04, 0x80, 0x05, 0xf6, + 0xa7, 0x00, 0x01, 0x01, 0x00, 0x02, 0x65, b'l', b'o', b'c', b'a', b'l', 0x03, 0x69, + b'm', b'e', b's', b's', b'a', b'g', b'e', b'-', b'1', 0x04, 0x18, 0x2a, 0x05, 0x19, + 0xea, 0x60, 0x06, 0xa0, ] ); assert_eq!( @@ -23,48 +33,57 @@ fn default_frame_encodes_as_deterministic_cbor() { } #[test] -fn event_head_frames_round_trip() { - let frame = RadrootsMeshFrame::new( - RadrootsMeshScope::Community, - vec![RadrootsMeshEventHead { - event_id: "event-1".to_string(), - author: "author-1".to_string(), - kind: 30818, - created_at: 1_725_000_000, - }], - ); - let encoded = encode_mesh_frame_cbor(&frame).expect("encode frame"); - let decoded = decode_mesh_frame_cbor(&encoded).expect("decode frame"); - - assert_eq!(decoded, frame); - assert_eq!(encode_mesh_frame_cbor(&decoded).expect("reencode"), encoded); -} +fn all_frame_types_round_trip_with_stable_codes_and_labels() { + let cases = [ + (RadrootsMeshFrameType::Hello, 0, "hello"), + ( + RadrootsMeshFrameType::EventHeadAnnounce, + 1, + "event_head_announce", + ), + (RadrootsMeshFrameType::EventRequest, 2, "event_request"), + (RadrootsMeshFrameType::EventChunk, 3, "event_chunk"), + (RadrootsMeshFrameType::EventAck, 4, "event_ack"), + (RadrootsMeshFrameType::RouteProbe, 5, "route_probe"), + ]; -#[test] -fn payload_transmission_is_forbidden_in_mvp_frames() { - let mut frame = RadrootsMeshFrame::new(RadrootsMeshScope::Local, Vec::new()); - frame.payload_policy = RadrootsMeshPayloadPolicy::PayloadTransmissionForbidden; - frame.payload = Some(vec![1, 2, 3]); + for (frame_type, code, label) in cases { + let frame = RadrootsMeshFrame::new( + frame_type, + RadrootsMeshScope::Community, + format!("{label}-message"), + u64::from(code) + 1, + 1_000, + ); + let encoded = encode_mesh_frame_cbor(&frame).expect("encode frame"); + let decoded = decode_mesh_frame_cbor(&encoded).expect("decode frame"); - assert_eq!( - encode_mesh_frame_cbor(&frame).expect_err("payload must fail"), - RadrootsMeshError::PayloadTransmissionForbidden - ); + assert_eq!(decoded, frame); + assert_eq!(encode_mesh_frame_cbor(&decoded).expect("reencode"), encoded); + assert_eq!(frame_type.code(), code); + assert_eq!(frame_type.label(), label); + } } #[test] fn custom_scope_has_explicit_namespace() { let scope = RadrootsMeshScope::custom(" Farm-North ").expect("custom scope"); assert_eq!(scope.label(), "farm-north"); - let frame = RadrootsMeshFrame::new(scope, Vec::new()); + let frame = RadrootsMeshFrame::new( + RadrootsMeshFrameType::RouteProbe, + scope, + "route-probe-1", + 10, + 1, + ); let encoded = encode_mesh_frame_cbor(&frame).expect("encode custom scope"); let decoded = decode_mesh_frame_cbor(&encoded).expect("decode custom scope"); - assert_eq!(decoded.scope.cbor_label(), "custom:farm-north"); + assert_eq!(decoded.scope_id.cbor_label(), "custom:farm-north"); } #[test] -fn mesh_scope_and_payload_policy_parsers_reject_unknown_values() { +fn mesh_parsers_and_validation_reject_unknown_or_empty_values() { assert_eq!( RadrootsMeshScope::custom(" ").expect_err("empty custom scope"), RadrootsMeshError::EmptyCustomScope @@ -73,15 +92,35 @@ fn mesh_scope_and_payload_policy_parsers_reject_unknown_values() { RadrootsMeshScope::parse("unscoped").expect_err("unknown scope"), RadrootsMeshError::UnknownScope ); + assert_eq!( + RadrootsMeshFrameType::parse_code(6).expect_err("unknown frame type"), + RadrootsMeshError::UnknownFrameType + ); assert_eq!(RadrootsMeshScope::Local.label(), "local"); assert_eq!(RadrootsMeshScope::Community.label(), "community"); + + let empty_message = RadrootsMeshFrame::new( + RadrootsMeshFrameType::Hello, + RadrootsMeshScope::Local, + " ", + 1, + 1, + ); assert_eq!( - RadrootsMeshPayloadPolicy::parse("inline-payloads").expect_err("unknown policy"), - RadrootsMeshError::UnknownPayloadPolicy + empty_message.validate().expect_err("empty message id"), + RadrootsMeshError::EmptyMessageId + ); + + let zero_ttl = RadrootsMeshFrame::new( + RadrootsMeshFrameType::Hello, + RadrootsMeshScope::Local, + "message-1", + 1, + 0, ); assert_eq!( - RadrootsMeshPayloadPolicy::PayloadTransmissionForbidden.label(), - "payload-forbidden" + zero_ttl.validate().expect_err("zero ttl"), + RadrootsMeshError::InvalidTtl ); } @@ -93,6 +132,11 @@ fn mesh_errors_have_stable_display_strings() { "mesh custom scope is empty", ), ( + RadrootsMeshError::EmptyMessageId, + "mesh message id is empty", + ), + (RadrootsMeshError::InvalidTtl, "mesh frame TTL is invalid"), + ( RadrootsMeshError::PayloadTransmissionForbidden, "mesh payload transmission is forbidden", ), @@ -101,11 +145,11 @@ fn mesh_errors_have_stable_display_strings() { RadrootsMeshError::InvalidUtf8, "mesh frame text is invalid UTF-8", ), - (RadrootsMeshError::UnknownScope, "mesh scope is unknown"), ( - RadrootsMeshError::UnknownPayloadPolicy, - "mesh payload policy is unknown", + RadrootsMeshError::UnknownFrameType, + "mesh frame type is unknown", ), + (RadrootsMeshError::UnknownScope, "mesh scope is unknown"), ( RadrootsMeshError::UnsupportedVersion, "mesh frame version is unsupported", @@ -118,15 +162,33 @@ fn mesh_errors_have_stable_display_strings() { } #[test] +fn payload_transmission_is_forbidden_in_mvp_frames() { + let mut frame = default_frame(); + frame.payload = RadrootsMeshPayload::Bytes(vec![1, 2, 3]); + + assert_eq!( + encode_mesh_frame_cbor(&frame).expect_err("payload must fail"), + RadrootsMeshError::PayloadTransmissionForbidden + ); + + let mut encoded_payload = encode_mesh_frame_cbor(&default_frame()).expect("encode default"); + let payload_offset = encoded_payload.len() - 1; + encoded_payload[payload_offset] = 0x43; + encoded_payload.extend_from_slice(&[1, 2, 3]); + assert_eq!( + decode_mesh_frame_cbor(&encoded_payload).expect_err("decode payload"), + RadrootsMeshError::PayloadTransmissionForbidden + ); +} + +#[test] fn cbor_codec_covers_extended_integer_widths() { let frame = RadrootsMeshFrame::new( + RadrootsMeshFrameType::EventAck, RadrootsMeshScope::Community, - vec![RadrootsMeshEventHead { - event_id: "event-with-wide-created-at".to_string(), - author: "author-with-wide-created-at".to_string(), - kind: 24, - created_at: u64::MAX, - }], + "wide-created-at", + u64::MAX, + u64::MAX, ); let encoded = encode_mesh_frame_cbor(&frame).expect("encode wide frame"); let decoded = decode_mesh_frame_cbor(&encoded).expect("decode wide frame"); @@ -135,44 +197,80 @@ fn cbor_codec_covers_extended_integer_widths() { } #[test] +fn decoder_rejects_previous_five_field_frame_shape() { + let previous_shape = vec![ + 0xa5, 0x01, 0x01, 0x02, 0x65, b'l', b'o', b'c', b'a', b'l', 0x03, 0x71, b'p', b'a', b'y', + b'l', b'o', b'a', b'd', b'-', b'f', b'o', b'r', b'b', b'i', b'd', b'd', b'e', b'n', 0x04, + 0x80, 0x05, 0xf6, + ]; + + assert_eq!( + decode_mesh_frame_cbor(&previous_shape).expect_err("old frame shape"), + RadrootsMeshError::InvalidCbor + ); +} + +#[test] fn decoder_rejects_malformed_cbor_shapes() { - let encoded = encode_mesh_frame_cbor(&RadrootsMeshFrame::new( - RadrootsMeshScope::Local, - Vec::new(), - )) - .expect("encode default"); + let encoded = encode_mesh_frame_cbor(&default_frame()).expect("encode default"); let mut unsupported_version = encoded.clone(); unsupported_version[2] = 2; assert_eq!( decode_mesh_frame_cbor(&unsupported_version).expect_err("unsupported version"), RadrootsMeshError::UnsupportedVersion ); - let mut frame = RadrootsMeshFrame::new(RadrootsMeshScope::Local, Vec::new()); - frame.version = 2; + + let mut out_of_range_version = encoded.clone(); + out_of_range_version.splice(2..3, [0x1a, 0x00, 0x01, 0x00, 0x01]); assert_eq!( - frame.validate().expect_err("unsupported frame version"), - RadrootsMeshError::UnsupportedVersion + decode_mesh_frame_cbor(&out_of_range_version).expect_err("out of range version"), + RadrootsMeshError::InvalidCbor + ); + + let mut unknown_frame_type = encoded.clone(); + unknown_frame_type[4] = 6; + assert_eq!( + decode_mesh_frame_cbor(&unknown_frame_type).expect_err("unknown frame type"), + RadrootsMeshError::UnknownFrameType + ); + + let mut zero_ttl = encoded.clone(); + zero_ttl.splice(27..30, [0x00]); + assert_eq!( + decode_mesh_frame_cbor(&zero_ttl).expect_err("zero ttl"), + RadrootsMeshError::InvalidTtl + ); + + let mut wrong_key_order = encoded.clone(); + wrong_key_order[3] = 2; + assert_eq!( + decode_mesh_frame_cbor(&wrong_key_order).expect_err("wrong key order"), + RadrootsMeshError::InvalidCbor ); let cases = [ vec![0x80], vec![0xbc], - vec![0xa4], - vec![0xa5, 0x02], - vec![0xa5, 0x01, 0x01, 0x02, 0x61, 0xff], + vec![0xa6], + vec![0xa7, 0x01], + vec![0xa7, 0x00, 0x01, 0x01, 0x00, 0x02, 0x61, 0xff], + vec![ + 0xa7, 0x00, 0x01, 0x01, 0x00, 0x02, 0x63, b'b', b'a', b'd', 0x03, 0x69, b'm', b'e', + b's', b's', b'a', b'g', b'e', b'-', b'1', 0x04, 0x18, 0x2a, 0x05, 0x19, 0xea, 0x60, + 0x06, 0xa0, + ], vec![ - 0xa5, 0x01, 0x01, 0x02, 0x63, b'b', b'a', b'd', 0x03, 0x71, b'p', b'a', b'y', b'l', - b'o', b'a', b'd', b'-', b'f', b'o', b'r', b'b', b'i', b'd', b'd', b'e', b'n', 0x04, - 0x80, 0x05, 0xf6, + 0xa7, 0x00, 0x01, 0x01, 0x00, 0x02, 0x7b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, ], vec![ - 0xa5, 0x01, 0x01, 0x02, 0x65, b'l', b'o', b'c', b'a', b'l', 0x03, 0x67, b'u', b'n', - b'k', b'n', b'o', b'w', b'n', 0x04, 0x80, 0x05, 0xf6, + 0xa7, 0x00, 0x01, 0x01, 0x00, 0x02, 0x65, b'l', b'o', b'c', b'a', b'l', 0x03, 0x60, + 0x04, 0x18, 0x2a, 0x05, 0x19, 0xea, 0x60, 0x06, 0xa0, ], vec![ - 0xa5, 0x01, 0x01, 0x02, 0x65, b'l', b'o', b'c', b'a', b'l', 0x03, 0x71, b'p', b'a', - b'y', b'l', b'o', b'a', b'd', b'-', b'f', b'o', b'r', b'b', b'i', b'd', b'd', b'e', - b'n', 0x04, 0x80, 0x05, 0x00, + 0xa7, 0x00, 0x01, 0x01, 0x00, 0x02, 0x65, b'l', b'o', b'c', b'a', b'l', 0x03, 0x69, + b'm', b'e', b's', b's', b'a', b'g', b'e', b'-', b'1', 0x04, 0x18, 0x2a, 0x05, 0x19, + 0xea, 0x60, 0x06, 0xa1, ], ];