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:
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,
],
];